该篇整理的是C++对象的动态建立和释放、对象的赋值和复制,这块比较好理解,大致了解下即可。
在C++中,对象的动态建立和释放通过使用new和delete操作符来完成的,new用于在运行时动态分配内存以创建对象,而delete用于释放这些动态分配的内存。对象的赋值和复制是两个不同的概念,赋值是通过操作符“=”进行的;复制是构造函数来实现的。
一、对象的动态建立和释放
使用new操作符可以动态分配内存并初始化对象,示例如下:
#include <iostream>
using namespace std;
class Student{
public:
Student(string n, int a): name(n), age(a){}
private:
string name;
int age;
};
int main(){
// 定义一个指向类Student的指针变量pt,并且赋值新建对象的起始地址
Student *pt = new Student("Tom", 20);
return 0;
}
调用对象即可以通过对象名,也可以通过指针。不过用new建立的动态对象一般是不用对象名,而是通过指针访问的。
在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。只要检查返回值是否为0,就可以判断分配内存是否成功。虽然C++标准提出,在执行new出现故障时,就“抛出”一个“异常”,用户可根据异常进行有关处理。但C++标准扔然允许在出现new故障时返回0指针值。当前,不同的编译系统对new故障的处理方法是不同的。
使用delete运算符释放用new建立的对象pt的空间,示例如下:
#include <iostream>
using namespace std;
class Student{
public:
Student(string n, int a): name(n), age(a){}
private:
string name;
int age;
};
int main(){
// 定义一个指向类Student的指针变量pt,并且赋值新建对象的起始地址
Student *pt = new Student("Tom", 20);
// 释放pt指向的内存空间
delete pt;
return 0;
}
在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成关有善后清理工作。
在释放内存后,指针本身不会被自动设置为nullptr。因此为了避免悬挂指针(dangling pointer,即指向已被释放内存的指针),通常建议在delete之后立即将指针设置为nullptr。示例如下:
#include <iostream>
using namespace std;
class Student{
public:
Student(string n, int a): name(n), age(a){}
private:
string name;
int age;
};
int main(){
// 定义一个指向类Student的指针变量pt,并且赋值新建对象的起始地址
Student *pt = new Student("Tom", 20);
cout <<"pt pointer:" <<pt <<endl;
// 释放pt指向的内存空间
delete pt;
cout <<"delete pointer:" <<pt <<endl;
pt = nullptr;
cout <<"null pointer:" <<pt <<endl;
return 0;
}
运行结果可以看出,在使用delete运算符释放内存空间后,指针还被悬挂着,通过赋值nullptr后返回0。如下图:
注意:nullptr,是c++中空指针类型的关键字,是在C++11中引入的,用来表示空指针类型。
二、对象的赋值和复制
对象的赋值和复制是两个不同的概念,它们通过赋值操作符“=”和复制构造函数实现。
2.1 赋值
赋值(Assignment)是通过赋值操作符“=”完成的,对于类对象,可以重载这个操作符以定义自己的赋值行为。示例代码如下:
#include <iostream>
using namespace std;
class Student{
public:
Student(){}
Student(string n, int a): name(n), age(a){}
void display(){
cout <<"name:" <<name <<", age:" <<age <<endl;
}
private:
string name;
int age;
};
int main(){
Student s1("Tom", 20), s2; //定义两个同类的对象
// 将对象s1赋给s2
s2 = s1;
// 显示结果
s1.display();
s2.display();
return 0;
}
运行结果可以看出,对象s1是通过深拷贝(memberwise copy)来完成的,是将对象s1中成员一一复制给对象s2。 如下图:
说明:
(1)对象赋值只对其中数据成员赋值,而不对成员函数赋值。数据成员是占内存储存空间,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一个对象的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段,不需要、也无法对它们赋值。
(2)类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。
2.2 复制
复制是通过复制构造函数完成的,复制构造函数是一从此特殊的构造函数(复制构造函数,copy constructor),它接受一个同类型的对象作为参数,并创建一个新的对象作为该对象的副本。复制构造函数实现原理代码如下:
// 复制构造函数定义
Student::Student(const Student &s){
name = s.name;
age = s.age;
}
复制构造函数也是构造函数,它只有一个参数,这个参数是本类的对象,而且采用引用的形式(一般约束加const声明,使参数值不能改变)。复制构造函数的作用是将实参对象的各数据成员值一一赋值给新的对象中对应的数据成员。
用户可以在声明类时定义复制构造函数,如果未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单的复制类中每个数据成员。示例代码如下:
#include <iostream>
using namespace std;
class Student{
public:
Student(){}
Student(string n, int a): name(n), age(a){}
void display(){
cout <<"name:" <<name <<", age:" <<age <<endl;
}
private:
string name;
int age;
};
int main(){
// 定义对象t1,并将对象t1复制给对象t2
Student t1("Tom", 20), t2(t1);
// 显示结果
t1.display();
t2.display();
return 0;
}
运行结果如下图:
这种形式可以看出,和上面讲的对象的赋值的概念上和语法的不同,但结果是一样的。对象的赋值是对一个已存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。
在以下三种情况下需要克隆对象:
- 程序中需要新建一个对象,并用另一个同类的对象对它初始化。
- 当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就按实参复制了一个形参,系统是通过调用复制构造函数来实现,这样能保证形参具有和实参完全相同的值。
- 函数的返回值是类的对象。在函数调用完毕需要将返回(对象)带回函数调用时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。