文章目录
封装篇(下)
1.对象成员的小总结
-
实例化对象A时,如果对象A有对象成员B,那么先执行对象B的构造函数,再执行A的构造函数。
-
如果对象A中有对象成员B,那么销毁对象A时,先执行对象A的析构函数,再执行B的析构函数。
-
如果对象A中有对象成员B,对象B没有默认构造函数,那么对象A必须在初始化列表中初始化对象B。
2.拷贝构造函数-浅拷贝与深拷贝
浅拷贝1:
Array.h
class Array {
public:
// 构造&拷贝构造&析构
Array();
Array(const Array&arr);
~Array();
// 属性操作方法
void setCount(int count);
int getCount();
private:
int m_iCount;
};
Array.c
// 构造&拷贝构造
Array::Array() {
cout << "Array" << endl;
}
Array::Array(const Array &arr) {
m_iCount = arr.m_iCount;
cout << "Array &" << endl;
}
// 析构
Array::~Array(){
cout << "~Array" << endl;
}
// 属性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}
main.c
int main(){
Array arr1;
arr1.setCount(5);
Array arr2(arr1);
cout << arr2.getCount() << endl;
return 0;
}
/*输出:
Array
Array &
5
~Array
~Array
*/
浅拷贝2:
此次在类中加入指针属性,所以需要注意的是关于类中指针的拷贝。
Array.h
class Array {
public:
Array(int count);
Array(const Array&arr);
~Array();
void setCount(int count);
int getCount();
void printAddr();
private:
int m_iCount; // 指针元素个数
int *m_pArr; // 用于指向数组的指针
};
Array.c
Array::Array(int count) {
// 为指针变量赋值
m_iCount = count;
m_pArr = new int[m_iCount];
// 给数组赋值,方便查看
for(int i = 0; i < m_iCount; i++){
m_pArr[i] = i;
}
cout << "Array" << endl;
}
// 只进行浅拷贝,导致指针只是直接拷贝了地址,
// 之后析构函数进行指针资源释放时会崩溃
Array::Array(const Array &arr) {
// 进行指针的拷贝(浅拷贝)
m_iCount = arr.m_iCount;
m_pArr = arr.m_pArr;
cout << "Array &" << endl;
}
Array::~Array(){
// 释放类中的指针属性
delete []m_pArr;
m_pArr = nullptr;
cout << "~Array" << endl;
}
// 属性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}
void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}
main.c
int main(){
Array arr1(5);
Array arr2(arr1);
arr1.printAddr();
arr2.printAddr();
return 0;
}
/*输出
Array
Array &
m_pArrd = 0xb516c0
m_pArrd = 0xb516c0
~Array
Process finished with exit code -1073740940 (0xC0000374)
*/
可见最后析构函数只完整运行了1次(输出一个~Array
),应为类中的指针都指向同一块内存地址,所以第二次析构函数进行指针资源释放会出现错误,导致程序崩溃。所以就需要用到深拷贝了。
深拷贝:
Array.h和main.c这两个文件的内容完全不变,只需要修改Array.c中拷贝构造函数即可。注意观察两个类中指针属性指向的地址。
Array.c
Array::Array(int count) {
m_iCount = count;
m_pArr = new int[m_iCount];
// 给数组赋值,可以方便后面进行输出查看
for(int i = 0; i < m_iCount; i++){
m_pArr[i] = i;
}
cout << "Array" << endl;
}
Array::Array(const Array &arr) {
m_iCount = arr.m_iCount;
// 新创建指针,开辟新的堆空间,
// 并将“被拷贝的类”中指针指向的数组的内容拷贝过来(深拷贝)
m_pArr = new int[m_iCount];
for(int i = 0; i < m_iCount; i++){
m_pArr[i] = arr.m_pArr[i];
}
cout << "Array &" << endl;
}
Array::~Array(){
delete []m_pArr;
m_pArr = nullptr;
cout << "~Array" << endl;
}
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}
void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}
main.c文件输出
/*输出
Array
Array &
m_pArrd = 0xa816c0
m_pArrd = 0xa816e0
~Array
~Array
*/
可以观察到,这两个类中的指针,指向的地址并不一样;所以析构函数能完整的运行两次,将所有的类中的指针属性正常释放掉。
3.this指针、对象指针
this指针的使用:
-
this指针无需用户定义,是编译器自动产生的。
-
this指针也是指针类型,所以在32位编译器下也占用4个基本的内存单元,即sizeof(this)的结果为4。
-
当成员函数的参数或临时变量与数据成员同名时,可以使用this指针区分同名的数据成员。
对上面的第3项举个例子:
如果我们在类中写了2个属性,分别是int x;int y;
,然后在这个类的构造函数中,括号里的形参名也是x和y,在这种情况下,我们就可以使用 this指针 代表 当前类的指针 ,this->x
就代表 当前类指针的x,这样就可以将临时变量x和当前类的x属性进行区分了。
Coordinate::Coordinate(int x,y){
this->x = x;
this->y = y;
}
这里只展示其中一种 对象指针 比较少用到的写法:
Coordinate p1;
Coordinate *p2 = &p1;
这里指针的写法和一般的变量一样。例如:int x=1; int *p = &x;
很多情况下都是 指针和堆内存 结合在一起使用的,但是这里是 指针和栈内存 结合在一起使用。
实例1,当 对象 作为 返回值 时:
要注意此时会调用拷贝构造函数,故会多生成一个对象,和原来的对象无关。如果我们对这个对象进行操作,之前的对象是不会受到任何影响的。
Array.h
class Array {
public:
Array(int len);
Array(const Array& arr);
~Array();
void setLen(int len);
int getLen();
Array printInfo();
private:
int m_iLen;
};
Array.c
Array::Array(int len){
m_iLen = len;
cout << "Array(int len)" << endl;
}
Array::Array(const Array &arr) {
cout << "Array(const Array &arr)" << endl;
}
Array::~Array(){
cout << "~Array()" << endl;
}
void Array::setLen(int len){m_iLen = len;}
int Array::getLen(){return m_iLen;}
// 当返回值是对象时,会调用拷贝构造函数,返回一个新的类,
// 所以程序结束的时候,会发现多调用了一次析构函数。
Array Array::printInfo(){
cout << "printInfo(),return Array" << endl;
return *this; // this是指针,*this是对象
}
main.c
int main() {
Array arr1(10);
// 对printInfo()返回的 【对象】 进行操作
//(调用getLen()将m_iLen变量设置为5),
// 但是此对象是通过拷贝构造函数新生成的,
// 所以通过arr1.getLen()输出的值是不会有变化的。
arr1.printInfo().setLen(5);
// 输出arr1这个对象的将m_iLen属性的值
cout << arr1.getLen() << endl;
return 0;
}
/* 输出:
Array(int len)
printInfo(),return Array
Array(const Array &arr)
~Array()
10
~Array()
*/
输出了两次~Array()
,说明在程序的运行过程中,确实出现了两个Array的对象。
通过printInfo()
和下一行的Array(const Array &arr)
可知,在进入到printInfo()
函数中返回Array对象之后,确实调用了拷贝构造函数。
提示:如何避免这种情况?那么就只有使用 指针 或 引用 了,之后的例子里会讲。
实例2,当 对象引用 作为 返回值 时:
修改一下Array类的声明和实现。
Array.h
//Array printInfo();
Array& printInfo();
Array.c
//Array Array::printInfo(){
// cout << "printInfo(),return Array" << endl;
// return *this; // this是指针,*this是对象
//}
// 当返回值是对象的引用时,就不会调用拷贝构造函数
Array& Array::printInfo(){
cout << "printInfo(),return Array&" << endl;
return *this; // this是指针,*this是对象
}
main.c
int main() {
Array arr1(10);
// 对返回的 【对象引用】 进行操作
// 但是此对象是
// 所以通过arr1.getLen()输出的值是不会有变化的。
arr1.printInfo().setLen(5);
// 输出arr1这个对象的将m_iLen属性的值
cout << arr1.getLen() << endl;
return 0;
}
/* 输出:
Array(int len)
printInfo(),return Array&
5
~Array()
*/
当传递对象的引用的时候,就不会调用拷贝构造函数创建一个新的对象了。而且通过引用我们可以对原对象()进行操作,就和我们使用指针的时候一样。
**返回对象引用的修改方法及其调用方法:**到此肯定注意到了在main中使用了arr1.printInfo().setLen(5)
这种调用方式。如果我们将其它函数改造成返回对象的引用return *this
,我们就可以使用这种调用方式,相当于每次返回的都是当前对象的引用,我们通过.
对这个对象引用中的函数进行调用。
举个例子,如果我们将setLen也改成返回对象的引用,那么我们就可以使用如下的调用方式:
arr1.printInfo().setLen(5).printInfo();
在这一连串的.
成员调用中,我们实际上都是对arr1这个对象中的属性/方法进行调用。
实例3,当 对象指针 作为 返回值 时:
将前面例2的代码由引用形式改成指针形式即可。
Array.h
//Array& printInfo();
Array* printInfo();
Array.c
// 当返回值是对象的引用时,就不会调用拷贝构造函数
//Array& Array::printInfo(){
// cout << "printInfo(),return Array&" << endl;
// return *this; // this是指针,*this是对象
//}
Array* Array::printInfo(){
cout << "printInfo(),return Array*" << endl;
return this; // this是指针,*this是对象
}
main.c
int main() {
Array arr1(10);
//arr1.printInfo().setLen(5);
// 对返回的 【指针】 进行操作,得到的结果和使用引用的一样
arr1.printInfo()->setLen(5);
// 输出arr1这个对象的将m_iLen属性的值
cout << arr1.getLen() << endl;
return 0;
}
/* 输出:
Array(int len)
printInfo(),return Array*
5
~Array()
*/
输出的内容和使用引用输出的一样,都是对arr1进行操作。
4.常量对象和常量成员函数之间的调用关系
1.如果我们用const修饰对象成员函数,会发现如果我们对函数体内的成员进行修改,会出现错误,代码如下:
// 错误
void Coordinate::changeX() const{
m_iX = 10;
}
// 如果不用const修饰则没有问题
void Coordinate::changeX(){
m_iX = 10;
}
2.上面的问题,和this指针有关,这里就来说明一下:
对于用户来说,编译器会帮我们添加this指针。
// 当我们定义changeX()这个成员函数的时候,看起来没有任何的参数
void Coordinate::changeX(){
m_iX = 10;
};
// 但是实际上却隐含着一个参数,就是this指针
void changeX(Coordinate *this){
this->m_iX = 10;
};
3.所以实际上代码为1中的代码是这种表现:
void Coordinate::changeX() const{
m_iX = 10;
}
// 实际表现如下:
void changeX(const Coordinate *this){
this->m_iX = 10;// 会出现错误
}
可以看出,对于changeX()
这个对象成员函数,使用 const 进行修饰,就相当于对 this指针 进行const修饰,原来的Coordinate *this
就被修饰成const Coordinate *this
,this是一个常量指针(既这个指针指向一个常量),所以我们不能对 this
指向的对象的内容进行修改,而m_iX
就是对象中的属性,所以我们当然不能对其进行修改了。
互为重载与其调用:
// 互为重载的声明
class Coordinate{
public:
Coordinate(int x, int y);
void changeX() const; // 和下一行的void changeX()互为重载
void changeX();
private:
int m_iX;
int m_iY;
}
// 调用void changeX()
int main(){
Coordinate coordinate(3,5);
coordinate.changeX();
return 0;
}
// 调用void changeX() const
int main(){
const Coordinate coordinate(3,5);// 在实例化对象的时候,加上const
coordinate.changeX();
return 0;
}
使用const实例化出来的对象,我们称为常量对象。
通过常量对象const Coordinate coordinate(3,5)
调用的就是常量对象函数void changeX() const
。
这里再说明一下常量对象与常量对象函数:
1.常量对象:
如果对象使用const修饰,那么就称为常量对象;常量对象和一般的常量(const int x之类)类似,其内容都是不可以修改的,也就是其中属性除了初始化的时候进行赋值,其它任何时候都不能对常量对象中的属性进行修改。
2.常量对象函数:
如果类中的函数采用const修饰,那么就称为常量对象函数;常量对像函数的隐含参数this指针,也会受到const的影响,导致this指针指向的对象的内容不可以修改(即通过这种方式,只有读权限,没有写权限)。
所以什么样的对象函数可以改成常量对象函数?
如果一个对象函数不需要对 对象的属性 进行写操作,那么就可以改为常量对象函数。
注意1:
将对象改成常量对象,对应的 获取对象属性的对象函数 也需要改成const
修饰的。(修改对象属性的对象函数没法改,原因看上面“所以什么样的对象函数可以改成常量对象函数?”这个问题)
这是因为这个对象函数如果不用const
修饰,那么隐含的this指针参数权限就是可读、可写的。而我们的常对象因为有const
限制,所以权限只有可读。所以 隐含的this指针 和 常量对象 的类型其实不同,也就不能用this指向常对象,否则会导致编译出现错误。
对应的程序代码在“04const2”
注意2:
当然,如果对象不是常量对象,对象函数对象函数可以用普通的对象函数,也可以用常量对象函数。
5.常量指针和常量引用的使用
普通的 对象引用 和 对象指针:
int main(){
Coordinate coor1(3,5);
Coordinate &coor2 = coor1; // 对象的引用
Coordinate *pCoor = &coor1; // 对象的指针
// 打印的都是coor1这种对象中的内容
coor1.printInfo();
coor2.printInfo();
pCoor->printInfo();
return 0;
}
常量指针和常量引用:
Coordinate.cpp
int Coordinate::getX(){
return m_iX;
}
int Coordinate::getY(){
return m_iY;
}
void Coordinate::printInfo() const {
cout << "(" << m_iX << "," << m_iY << ")" << endl;
}
main.c
int main(){
Coordinate coor1(3,5);
const Coordinate &coor2 = coor1; // 常量对象的引用
const Coordinate *pCoor = &coor1; // 常量对象的指针
coor1.printInfo();
coor2.getX(); // 报错
pCoor->getY(); // 报错,'this' argument to member function 'getY' has type 'const Coordinate', but function is not marked const
return 0;
}
和之前说的同理,引用和指针加上const修饰,就只有可读权限,而getX()
和getY()
都是普通对象函数,其中隐含的this指针为可读、可写权限。类型不同,当然会报错了。
上面报错的翻译:
‘this’ argument to member function ‘getY’ has type ‘const Coordinate’, but function is not marked const
成员函数“getY”的这个参数的类型是“const Coordinate”,但是函数没有标记const
故,如果函数也用const修饰,则可以正常运行。
6.对象指针常量的使用
Coordinate.cpp
int Coordinate::getY(){
return m_iY;
}
void Coordinate::printInfo() const {
cout << "(" << m_iX << "," << m_iY << ")" << endl;
}
main.c
int main(){
Coordinate coor1(3,5);
Coordinate coor2(7,9);
Coordinate * const pCoor = &coor1;
pCoor->getY(); // 正常调用
pCoor = coor2; // 错误
pCoor->printInfo(); // 正常调用
return 0;
}
pCoor是一个指针常量,本身不可以修改,但是指向的内存上的内容是可以修改的,也就是说具有可读、可写的权限。
-
pCoor->getY():
这里是符合getY()
这个对象函数的要求的,所以可以正常调用。 -
pCoor = coor2
:这里很明显的是要改变pCoor的指向,但是pCoor是常量,无法改变其指向的地址,所以是错误的。 -
pCoor->printInfo()
:这个在本文第4章末尾的 注意2 有提及,对象是可读、可写的,而printInfo()
中this指针只要求是可读的,所以当然也是可以正常调用的。
7.第5、6章小总结
- 常量对象只能调用常量成员函数,不能调用普通成员函数。
- 普通对象能够调用常量成员函数,也能够调用普通成员函数。
- 常量指针和常引用都只能调用对象的常量成员函数。
本篇为视频教程笔记,视频如下: