C++动态内存分配超详细讲解

 更新时间:2022年08月22日 09:39:42   作者:Shawn-Summer  
给数组分配多大的空间?你是否和初学C时的我一样,有过这样的疑问。这一期就来聊一聊动态内存的分配,读完这篇文章,你可能对内存的分配有一个更好的理解

1.在类中使用动态内存分配的注意事项

1.1 构造函数中使用new

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
  • newdelete必须相互兼容,new相对delete;new[]相对delete[]
  • 因为只有一个析构函数,所有的构造函数都必须与它兼容

注意的是:delete或者delete[]都可以对空指针操作.

NULl0nullptr:空指针可以用0或者NULL来表示,C++11使用一个特殊的关键词:nullptr来表示空指针.

应该定义一个复制构造函数,通过深度复制将一个对象初始化成另一个对象.

String::String(const String &st)//复制构造函数
{
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
    num_strings++;
}

应该定义一个赋值运算符。

String& String::operator=(const String& st)//赋值运算符
{
    if(this==&st)
    return *this;
    delete[] str;
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
    return *this;
}

具体来说,操作是:检查自我赋值情况,释放成员指针以前指向的内存,复制数据而不仅仅是地址,返回一个指向调用对象的引用.

一个典型错误

String::String()
{
    str="default string";
    len=std::strlen(str);
}

上面这段代码定义了默认构造函数,但是它犯了一个错误:无法和析构函数中的delete[]匹配.

包含类成员的类的逐成员复制

class Magazine
{
    private:
        String title;
        String publisher;
}

类成员的类型是String,这是否意味着要为Magazine类编写复制构造函数和赋值运算符?不.

如果你将一个Magazine对象复制或者赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符.也就是说复制title时,将调用String的复制构造函数,而将title赋值给另一个Magazine对象时,也会使用String的赋值运算符.

1.2 有关返回对象的说明

返回指向const对象的引用

返回对象会调用复制构造函数生成临时对象,而返回const对象的引用不会.

引用指向的对象不能是局部变量. 总之,返回指向const对象的引用,就是按值传递的升级版,但是它不能返回局部变量.

返回指向非const对象的引用

例如我们重载<<时,

ostream& operator<<(ostream & os,class_name object); 返回指向非const对象的引用,主要是我们希望对函数返回对象进行修改.

返回对象

就是按值传递.

如果我们返回的对象是局部变量,那么我们不能使用引用来返回了,只能采用返回对象.

返回const对象

不太常用.防止用户对临时对象进行赋值操作,而编译器不会对这种操作报错.

总之,如果要返回局部对象就必须返回对象;如果,那必须返回对象的引用;如果返回对象也行,返回指向对象的引用也行,那优先使用引用版本,因为效率更高.

1.3 使用new创建对象

String * glop=new String("my my my");

这句话会使用构造函数String(const char *);

glop->类成员

可以使用这种方式调用对象成员,学过C语言的应该明白。

对于动态分配的对象,它的析构函数当且仅当使用delete删除对象时,它的析构函数才会调用。

定位new的用法

#include<iostream>
#include<string>
#include<new>
using std::string;
using std::cout;
using std::cin;
using std::endl;
const int BUF=512;
class JustTesting
{
    private:
        string words;
        int number;
    public:
        JustTesting(const string & s="Just Testing",int n=0)
            :words(s),number(n){cout<<words<<" constructed.\n";}
        ~JustTesting(){cout<<words<<" destoryed!\n";}
        void show() const {cout<<words<<", "<<number<<endl;}
};
int main()
{
    char * buffer=new char[BUF];//获得一块512B内存
    JustTesting *pc1,*pc2;
    pc1=new(buffer) JustTesting;//在该块内存中分配空间
    pc2=new JustTesting ("Heap1",20);
    cout<<"Memory block addresses:\n"<<"buffer: "<<(void*)buffer<<" heap: "<<pc2<<endl;
    cout<<"Memory contents:\n";
    cout<<pc1<<": ";
    pc1->show();
    cout<<pc2<<": ";
    pc2->show();
    JustTesting *pc3,*pc4;
    pc3=new(buffer+sizeof(JustTesting)) JustTesting ("Bad Idea",6);
    pc4=new JustTesting ("Heap2",10);
    cout<<"Memory contents:\n";
    cout<<pc3<<": ";
    pc3->show();
    cout<<pc4<<": ";
    pc4->show();
    delete pc2;
    delete pc4;
    pc3->~JustTesting();
    pc1->~JustTesting();
    delete [] buffer;
    cout<<"done !\n";
}

Just Testing constructed.
Heap1 constructed.
Memory block addresses:
buffer: 0xf040a0 heap: 0xf042d0
Memory contents:
0xf040a0: Just Testing, 0
0xf042d0: Heap1, 20
Bad Idea constructed.
Heap2 constructed.
Memory contents:
0xf040c8: Bad Idea, 6
0xf04330: Heap2, 10
Heap1 destoryed!
Heap2 destoryed!
Bad Idea destoryed!
Just Testing destoryed!
done !

上面这段代码演示了定位new的用法,这个我们之前在内存模型中谈过。这里需要注意的是,如果使用定位new创建对象,如何确保其析构函数被调用,我们不能使用delete p3;delete p1;,这是因为delete和定位new不匹配,我们必须显式调用析构函数p1->~JustTesting();

2.队列模拟

和栈(Stack)一样,队列(Queue)也是一个很重要的抽象数据结构。这一节将会构建一个Queue类,顺便复习之前所学的技术和学习少量新知识。

我们采用链表来实现队列。

2.1 类声明中的一些思考

typedef  std::string Item;
class Queue
{
    private:
        struct Node
        {
            Item item;
            struct Node *next;
        };
        enum{Q_SIZE=10};
        Node* front;//队首指针
        Node* rear;//队尾指针
        int items;//队列中的元素个数
        const int qsize;//队列的最大元素个数
        //抢占式定义
        Queue(const Queue & q):qsize(0){}
        Queue & operator=(const Queue & q){return *this;}
    public:
        Queue(int qs=Q_SIZE);
        ~Queue();
        bool isempty() const;//空
        bool isfull() const;//满
        int queuecount() const;//队列中元素个数
        bool enqueue(const Item &i);//入队
        bool dequeue(Item & i);//出队
        void show() const;        
};

类作用域中的结构体

类似于类作用域中的常量,通过将结构体Node声明放在Queue类的私有部分,就可以在类作用域中使用该结构体。这样就不用担心,Node声明和某些全局声明发生冲突。此外,类声明中还能使用Typedef或者namespace等声明,都可以使其作用域变成类中。

利用构造函数初始化const数据成员

在类中qsize是队列最大元素个数,它是个常量数据成员

Queue::Queue(int qs)
{
    qsize=qs;
    front =rear=nullptr;
    items=0;
}

上面这段代码是错误的。因为常量是不允许被赋值的。C++提供了一种新的方式来解决这一问题–成员初始化列表。

成员初始化列表语法

它的作用是,在调用构造函数的时候,能够初始化数据。对于const类成员,引用数据成员,都应该使用这种语法。

于是,构造函数可以这样:

Queue::Queue(int qs):qsize(qs)
{
    front =rear=nullptr;
    items=0;
}

而且这种方法不限于初始化常量,还能初始化非const变量。则构造函数也可以这样:

Queue::Queue(int qs):qsize(qs),front(nullptr),rear(nullptr),items(0){}

但是,成员初始化列表语法只能用于构造函数。

类内初始化

在C++中,其实还有一种更直观的初始化方式,那就是直接在类声明中进行初始化。

class Classy
{
  int mem1=10;
  const int mem2=20;
};

相当于在构造函数中使用

Classy::Classy():mem1(10),mem2(20){...}

但是如果你同时使用类内初始化和成员列表语法时,调用相应构造函数时,成员列表语法会覆盖类内初始化。

Classy::Classy(int n):mem1(n){...}

调用上面这个构造函数时,mem1会被设置成n,而mem2由于类内初始化的原因被设置成20.

是否需要显式析构函数?

Queue类的构造函数中是不需要使用new的,因为构造函数只是构造一个空队列,那这是不是意味著不需要在析构函数中使用delete?

我们知道,虽然构造函数不需要new,但是在enqueue入队时,我们需要new一个新元素加入队列。那么我们必须在析构函数中使用delete以确保所有动态分配的空间被释放。

伪私有方法(抢占式定义)

既然我们在Queue类中,使用了动态内存分配,那么编译器提供的默认复制构造函数,和默认赋值运算符是不正确的。我们假设队列是不允许被赋值或者复制的,那么我们可以使用伪私有方法,目的是禁用某些默认接口。

class Queue
{
  private:
    Queue(const Queue & q):qsize(0){}
    Queue & operator=(const Queue & q){return *this;}
}

这样做的原理是:在私有部分抢先定义了复制构造函数,赋值运算符,那么编译器就不会提供默认方法了,那么对象就无法调用这些方法。

C++提供了另一种禁用方法的方式–使用关键词delete

class Queue
{
  public:
    Queue(const Queue & q)=delete;
    Queue & operator=(const Queue & q)=delete;
}

可以直接在公有部分中禁用某种方法。

2.2 代码实现

//queue.h
#ifndef QUEUE_H_
#define QUEUE_H_
#include<string>
typedef  std::string Item;
class Queue
{
    private:
        struct Node
        {
            Item item;
            struct Node *next;
        };
        enum{Q_SIZE=10};
        Node* front;//队首指针
        Node* rear;//队尾指针
        int items;//队列中的元素个数
        const int qsize;//队列的最大元素个数
        //抢占式定义
        Queue(const Queue & q):qsize(0){}
        Queue & operator=(const Queue & q){return *this;}
    public:
        Queue(int qs=Q_SIZE);
        ~Queue();
        bool isempty() const;//空
        bool isfull() const;//满
        int queuecount() const;//队列中元素个数
        bool enqueue(const Item &i);//入队
        bool dequeue(Item & i);//出队
        void show() const;        
};
#endif
//queue.cpp
#include"queue.h"
#include<iostream>
Queue::Queue(int qs):qsize(qs)
{
    front =rear=nullptr;
    items=0;
}
Queue::~Queue()
{
    Node * p;
    while (front!=nullptr)
    {
        p=front;
        front=front->next;
        delete p;
    }
}
bool Queue::isempty() const
{
    return items==0;
}
bool Queue::isfull() const
{
    return items==qsize;
}
int Queue::queuecount() const
{
    return items;
}
bool Queue::enqueue(const Item &i)
{
    if(isfull())
        return false;
    Node *add=new Node;
    add->item=i;
    add->next=nullptr;
    items++;
    if(front==nullptr)//队空
        front=rear=add;
    else
    {
        rear->next=add;
        rear=add;
    }
    return true;
}
bool Queue::dequeue(Item & i)
{
    if(isempty())
        return false;
    i=front->item;
    items--;
    if(items==0)
    {
        delete front;
        front=rear=nullptr;
    }
    else
    {
        Node *p=front;
        front=front->next;
        delete p;
    }
    return true;
} 
void Queue::show() const
{
    using std::cout;
    using std::endl;
    cout<<"the items: "<<items<<endl;
    if(isempty())
        cout<<"Empty queue!\n";
    else
    {
        cout<<"front: ";
        for(Node*p=front;p!=nullptr;p=p->next)
        {
            cout<<p->item;
            if(p!=rear)
                cout<<"-> ";
        }
        cout<<" :rear\n";
    }
}
//queuetest.cpp
#include"queue.h"
#include<iostream>
int main()
{
    using std::cin;
    using std::cout;
    using std::endl;
    using std::string;
    Queue test(8);
    char choice;
    cout<<"Enter E to enqueue ,D to dequeue,Q to quit: ";
    while(cin>>choice)
    {
        string temp;
        switch (choice)
        {
        case 'E':
            cout<<"Enter the string: ";
            cin>>temp;
            if(test.enqueue(temp))
                test.show();
            else
                cout<<"can't enqueue\n";
            break;
        case 'D':
            if (test.dequeue(temp))
            {
                cout<<"the item gotten: "<<temp<<endl;
                test.show();
            }
            else
                cout<<"can't dequeue\n";
            break;
        case 'Q':
            goto aa;
            break;
        default:
            break;
        }
        cout<<endl;
        cout<<"Enter E to enqueue ,D to dequeue,Q to quit: ";
        cin.ignore();
    }
    aa:test.~Queue();
    cout<<"Bye!: ";
    test.show();
}

PS D:\study\c++\path_to_c++> .\queue.exe
Enter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: apple
the items: 1
front: apple :rear

Enter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: banana
the items: 2
front: apple-> banana :rear

Enter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: candy
the items: 3
front: apple-> banana-> candy :rear

Enter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: dizzy
the items: 4
front: apple-> banana-> candy-> dizzy :rear

Enter E to enqueue ,D to dequeue,Q to quit: D
the item gotten: apple
the items: 3
front: banana-> candy-> dizzy :rear

Enter E to enqueue ,D to dequeue,Q to quit: D
the item gotten: banana
the items: 2
front: candy-> dizzy :rear

Enter E to enqueue ,D to dequeue,Q to quit: Q
Bye!: the items: 2
front:  :rear

到此这篇关于C++动态内存分配超详细讲解的文章就介绍到这了,更多相关C++动态内存分配内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论