C++动态内存分配
Shawn-Summer 人气:01.在类中使用动态内存分配的注意事项
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
加载全部内容