C++动态内存分配超详细讲解
1.在类中使用动态内存分配的注意事项
1.1 构造函数中使用new
- 如果在构造函数中使用
new
来初始化指针成员,则应在析构函数中使用delete
new
和delete
必须相互兼容,new
相对delete
;new[]
相对delete[]
- 因为只有一个析构函数,所有的构造函数都必须与它兼容
注意的是:delete
或者delete[]
都可以对空指针操作.
NULl
和0
和nullptr
:空指针可以用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 :rearEnter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: banana
the items: 2
front: apple-> banana :rearEnter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: candy
the items: 3
front: apple-> banana-> candy :rearEnter E to enqueue ,D to dequeue,Q to quit: E
Enter the string: dizzy
the items: 4
front: apple-> banana-> candy-> dizzy :rearEnter E to enqueue ,D to dequeue,Q to quit: D
the item gotten: apple
the items: 3
front: banana-> candy-> dizzy :rearEnter E to enqueue ,D to dequeue,Q to quit: D
the item gotten: banana
the items: 2
front: candy-> dizzy :rearEnter E to enqueue ,D to dequeue,Q to quit: Q
Bye!: the items: 2
front: :rear
到此这篇关于C++动态内存分配超详细讲解的文章就介绍到这了,更多相关C++动态内存分配内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
VS2010+Opencv+MFC读取图像和视频显示在Picture控件
这篇文章主要为大家详细介绍了VS2010+Opencv+MFC读取图像和视频显示在Picture控件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2019-08-08
最新评论