C++基础(二)封装

1. 构造函数与析构函数

  1. 类内定义的函数 编译器会以内联函数的方式编译
  2. 内存分区
    栈区:int x=0; int *p=NULL 内存由系统分配和回收
    堆区:int *p=new int[20] 程序员管理
    全局区:存储全局变量和静态变量
    常量区:string s="hello"
    代码区:存储逻辑代码的二进制
  3. 类在实例化之前不会占用堆或栈的内存
  4. 对象初始化可以有且仅有一次(构造函数),也可以根据条件初始化
  5. 构造函数的规则:对象实例化时自动调用,构造函数与类同名,构造函数没有返回值,构造函数可以有多个重载形式,实例化对象时只用到一个构造函数,当用户没有定义构造函数时,编译器自动生成一个不做任何事的构造函数
  6. 在实例化对象时不需要传参数的构造函数叫默认构造函数,包括无参构造函数和参数都有默认值的有参构造函数
  7. 初始化列表 Student():m_name("Jim"),m_age(10){} 初始化列表先于构造函数执行,初始化列表只能用于构造函数,可以同时初始化多个数据成员,并且效率高,const类型的成员变量只能用初始化列表的方式初始化
  8. 拷贝构造函数:形如Student stu2=stu1Student stu3(stu1)这样的形式会调用拷贝构造函数。拷贝构造函数的定义方式:Student(const Student& stu){}。如果没有定义拷贝构造函数,系统会自动生成一个默认的拷贝构造函数。
    构造函数总结如下图:
    在这里插入图片描述
    在这里插入图片描述
  9. 析构函数:~Student(){}没有返回值,没有参数,不能重载,在对象销毁时自动调用,如果没有定义析构函数,系统会自动生成一个默认的析构函数

2. 对象数组

//栈中实例化对象数组
Coordinate coord[3];
coord[1].m_x=10;
//堆中实例化对象数组
Coordinate *p=new Coordinate[3];
p[0].m_x=10;
p->m_x=10;
delete []p;
p=NULL;

一个从堆中实例化的例子:
在这里插入图片描述

Coordinate *p =new Coordinate[3];
p->m_iX = 7; //直接写p的话,就说明是第一个元素
p[0].m_iY =9; //等价于 p->m_iY = 9

p++; //p指向第2个元素
p->m_iX = 11;
p[0].m_iY = 13; //这里p指向的是第二个元素,p[0]就是当前元素,等价于p->m_iY = 13

p[1].m_iX = 15;//第3个元素的横坐标
p++; //p当前还是指向第二个元素,p++将指针后移一个位置,p指向第3个元素
p[0].m_iY = 17;//这里p指向的是第三个元素,p[0]就是当前元素,等价于p->m_iY = 17

for(int j = 0; j < 3; j++)
{
    //如果上面p没有经过++操作,就可以按下面来轮询
    //cout <<"p_X: " << p[i].m_iX <<endl;
    //cout <<"p_Y: " << p[i].m_iY <<endl;
    //但是,上面我们对p做了两次++操作,实际p已经指向了第3个元素,应如下操作
    cout <<"p_X: "<< p->m_iX <<endl;
    cout <<"p_Y: "<< p->m_iY <<endl;
    p--;
}

//经过了上面三次循环后,p指向了一个非法内存,不能直接就delete,而应该让p再指向我们申请的一个元素的,如下
    p++; //这样p就指向了我们申请的内存
    delete []p;
    p = NULL;

3. 对象成员

当类中有对象成员时,实例化这个类和析构这个类,类和对象成员的构造顺序和销毁顺序是怎样的呢?
举个例子,Line中有两个Coordinate类的对象,m_CoorA和m_CoorB。
在这里插入图片描述
当实例化Line对象时,执行Line* p=new Line()先实例化m_CoorA,再实例化m_CoorB,最后实例化Line对象,销毁的时候顺序相反,先销毁Line对象,再销毁m_CoorB,最后销毁m_CoorA
另外,实例化时的参数怎么设置呢?
Coordinate类的构造函数Coordinate(int x, int y) ,Line类的构造函数可以用初始化列表的方式Line(int x1,int y1,int x2,int y2):m_CoorA(x1,y1),m_CoorB(x2,y2){},当然也可以用别的方式,这样就可以在main函数中Line* p=new Line(1,2,3,4);这样实例化对象。

4. 深拷贝与浅拷贝

浅拷贝只将数据成员的值进行简单的拷贝。遇到指针的时候会有问题,看下面的例子
在这里插入图片描述
这样arr1和arr2的m_pArr会指向同一个内存,对任何一个进行修改都会影响到另一个,但实际上这两个值应该是独立的。
在这里插入图片描述
更严重的是,在销毁arr1和arr2的时候,这块内存会被释放两次,计算机会崩溃。
我们希望的应该是arr1和arr2的m_pArr指向不同的地址。实现方法如下图:
在这里插入图片描述
这种拷贝就叫深拷贝。

5. 对象指针

实例化对象可以用指针的方式,在堆中实例化。下面的例子中,p指向的其实就是第一个数据成员的地址。记得在堆中申请的内存最后要释放。
在这里插入图片描述

6. 对象成员指针

和刚刚的对象成员不同,这里讲的是对象的指针作为成员。
在这里插入图片描述
和之前的对象成员不同,之前如果用sizeof操作符看Line对象的大小,结果应该是16,因为有两个Coordinate成员对象,每个Coordinate有两个int类型的成员m_iX和m_iY。现在这里如果用sizeof操作符看Line对象的大小,结果是8,因为不管是什么对象的指针,都是4个字节,指针指向的m_iX和m_iY都在堆中,并不在Line对象内存里。
再来看生成和销毁过程有什么不同。这里实例化Line对象时,对象成员指针也就被创建,然后Line再被实例化,销毁时先释放堆中的内存,即指针指向的内存,然后再释放Line对象。

7. this指针

类中参数是不能与成员变量同名的,可以用this指针。对象的this指针指向这个对象的地址,即Array arr; this等价于&arr。如果想用与成员变量同名的参数,就可以写成Array(int len){this->len=len;}
这里想一个问题,一个类的成员函数是在代码区,每次操作一个对象时都调用的同一份代码,那么成员函数是如何访问到对应的数据成员的呢?其实每个成员函数都有一个隐藏的this指针参数,指向当前对象的地址。
this指针还有一个好玩的用法。像下面这样定义Array类的print成员函数:

Array Array::print()
{
	cout<<len<<endl;
	return *this;
}//this是指针,*this就是Array对象

在main函数中这样用:

Array arr(10);
arr.print().setLen(5);
cout<<arr.getLen()<<endl;

发现结果是打印出两个10,说明setLen并没有对arr起到作用。为什么呢?因为print函数返回的*this是一个临时的对象,并不是arr。为什么是临时变量呢?可以这么理解:

Array Array::print(){
	return *this;
}
//等价于
Array tmp=Array(arr->*this);
//等价于
Array tmp=Array(arr);
//即为一个拷贝构造出的临时变量,对临时变量的操作不会影响到源数据本身

那么解决方法是什么?我们想到了引用和指针。把Array Array::print()改为Array& Array::print(),这就等价于Array &tmp=arr;;如果改成指针,就是Array* Array::print(),return也要改成return this;,调用的时候就要用->,等价于Array *tmp=&arr;
如果同理改造一下setLen,就可以像arr.print().setLen(5).print()这样连续调用了。

8. 再论const

  1. 常对象成员和常成员函数
    常对象成员就是对象成员前面加个const,这样构造函数就只能用初始化列表的方式,参考对象成员那部分。
    在成员函数后面加const就是常成员函数,常成员函数中不能修改成员函数的值,为什么呢?以Coordinate类为例:
//普通成员函数
void Coordinate::changeX(Coordinate *this)
		|
		V
void changeX(Coordinate *this)
{ this->m_iX=10; }
//常成员函数
void Coordinate::changeX() const//声明和定义都要加const
{ m_iX=10; }
		|
		V
void changeX(const Coordinate *this)//常指针改变数据是不允许的
{ this->m_iX=10; }		

void changeX() const;void changeX();其实是可以同时存在的,他们互为重载,但没必要这么做。如果这么做,那么Coordinate coor(3,5); coor.changeX();调用的是哪个changeX?这里调用的是不带const的成员函数。如果定义的是常对象const Coordinate coor(3,5);那么调用的changeX就是常成员函数。
2. 常指针与常引用
Coordinate类中,把print成员函数定义成const:

void print() const;

main函数中:

Coordinate coor1(3,5);
const Coordinate &coor2=coor1;//常引用
const Coordinate *pCoor=&coor1;//常指针
coor1.print();//可以
coor2.getX();//getX不是常成员函数,会出错,因为getX有读写权限,而coor2只有读权限,只能调用常成员函数
pCoor->getY();//getY不是常成员函数,同理会出错,只能调用常成员函数

更复杂的情况:

Coordinate coor1(3,5);
Coordinate coor2(7,9);
Coordinate* const pCoor=&coor1;//const位置不一样了,在*后面,表示不能指向其它对象
pCoor->getY();//可以,指针指向的内容本身是可变的,指针指向的内容有读写权限
pCoor=coor2;//出错,不能指向其它对象
pCoor->print();//可以,print是常成员函数,this指针有读权限,而pCoor有读写权限,是正确的

整理自慕课网c++远征

猜你喜欢

转载自blog.csdn.net/weixin_43927408/article/details/87997645