C++(2)——构造与析构函数,new与malloc,free与delete,引用(深入),拷贝构造函数

构造函数

若对象的属性成员公有时,可以通过{" "}直接赋值,并且只要有一个数据成员为私有,都不可以使用该方法进行初始化,但数据成员多为私有的,要对它们进行初始化,必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次。称为构造函数(constructor)
构造函数不由用户调动,而是由系统(编译器)自动调动
构造函数的用途:

  1. 创建对象(注意:不是创建空间)
  2. 初始化对象中的属性
  3. 类型转换

因此,程序一旦启动,会分配给这一进程一定的空间,但是有空间不一定有对象,必须通过调动构造函数创建不同对象。

构造函数的特征如下:
构造函数是特殊的公有成员函数(99%的情况下,特殊用途下会被设计为私有或保护)

  1. 函数名与类名相同
  2. 构造函数无函数返回类型说明,不是void(实际上构造函数有返回值,返回的就是构造函数所创建的对象)
  3. 在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,在该对象生存期中也只被系统调用这一次,永远无法通过对象自身自己调动。
  4. 构造函数可以重载。严格地讲,类中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。
  5. 构造函数可以在类中定义,也可以在类中声明,在类外定义,使用作用域解析符::
  6. 如果类中说明没有给出构造函数,则C++编译器会自动给出一个缺省的构造函数:
    ——类名(void){}

但只要我们定义了一个构造函数,系统就不会自动生成缺省的构造函数。只要构造函数是无参或者只要各参数均有缺省值的,C++编译器都会认为是缺省的构造函数,并且缺省的构造函数只有一个。

大致形式如下:

class Pointer
{
	int row;
	int col
	public:
	//无参构造函数
		Pointer(){}
	//调动缺省构造函数时,不要加括号,例如:Pointer c1;
	//一旦没有实参却加了括号,C++为了兼容于C,就会将其识别为函数的声明
	
	//有参构造函数
		Pointer(int r,int c)
		{
			row = r;
			col = c;
		}
	//调动有参构造函数时,带括号就必须传入实!!例如:Pointer c(1,2);
	
	//还可以使用如下的写法:
	Pointer C = Pointer(2,4);//等同于Pointer C(2,4);

结论:当创建一个对象时,有实参则在括号中注明,调用构造函数,如果没有实参就什么都不写,不加括号,从而调用缺省构造函数,加了括号的写法会被认为是函数的声明。

同时,对象本身不能调动构造函数,构造函数由系统调用

例如:
对于一个类CGoods

CGoods z;
z.CGoods("c#",100,128);//这种写法是不被允许的

构造函数中,参数均有缺省值时,该构造函数就会成为缺省构造函数

class CGoods
{
	CGoods(char *name = NULL,float price = 0)
	{
		strcpy(Name,name);
		Price = price;
		Amount = 0;
		Total_value = 0.0;
	}
	CGoods()
	{
		Name[0] = '\0';
		Price = 0;
		Amount = 0;
		Total_value = 0.0;
	}
}

对于如下的类:

class Object:
{
	int value;
public:
	Object(int x = 0):value(x){}
	//等同于{value = x;}
};

注意:对于构造函数和拷贝构造函数可以使用列表方式初始化对象,其他类的成员函数不可以使用列表方式,看下面的代码,使用了列表方式创建对象

class Rectangle
{
private:
    int left;           
    int top;         
    int right;        
    int bottom;  
public:
    // 一次定义默认构造函数和带参的构造函数
   Rectangle(int e = 0, int t = 0, int  r = 0, int b = 0) : left(e), top(t), right(r), bottom(b) {}
 }

其他成员函数在使用列表方式时,有的时候会产生二次调用构造函数的影响,这样是不被允许的;

class Object
{
private:
	int val;
public:
	Object(int x):val(x){}
	~Object(){}
};
class Tset
{
	Object obj:
public:
	Tset(int x):obj(x){}
	void SetValue:obj(x){}
};

int main()
{
	Test t(10);
	t.SetvValue(18);
}

上述程序在对象t的空间内创建了对象obj,初始化val = 10,之后调用SetValue方法,相当于再次调用了构造函数,但是构造函数只会被调用一次,不能重生。
构造函数——类型装换
构造函数的第三个任务,我们这里还没有说明,请看为何下面的程序能够成功运行:

class Int
{
private:
    int value;
public:
    Int(int x = 0) :value(x) { cout << "Create Int: " << this << endl; }

    Int(const Int& it) :value(it.value) { cout << "Copy Create Int: " << this << endl; }

    ~Int() { cout << "Destroy Int : " << this << endl; }
}int main()
{
    Int a = 10;
    int b = 20;
    a = b;
    return 0;
}

我们来看运行结果:
在这里插入图片描述
因此,对于所属对象为单参的构造函数在这个过程中对b进行了类型转换,但是其实这一过程中发生了隐式转换,explicit就是为了防止构造函数的这种隐式转换,为了避免这种现象的发生,我们可以加上关键字explicit,修改代码如下;

 explicit Int(int x = 0) :value(x) { cout << "Create Int: " << this << endl; }

之后还要完成上述操作,就必须进行强转:

a = Int(b);

对于双参的对象,自然无法完成类似的操作。

然而,在有些编译器之下,对于如下的对象,可变相地理解为单参的对象,从而完成类型转换;

 Int(int x,int y = 0) :value(x+y) { cout << "Create Int: " << this << endl; }
 
 	Int a(1,2);
    int b = 100;
    
    //按照类型转换方式创建了对象,要求构造函数必须是单参
    a = b,20;//值为20,这里是强转,相当于a = (Int)(b,20),取逗号表达式最右边的值赋值给x,y默认为0
    //注意;必须是单参!!!

	//调动构造函数创建了对象
	a = Int(b,20);//调动构造函数,创建无名对象b赋值给x,20赋值给y,a为120

反之,我们能够将一个对象内的某个属性值赋值给一个变量呢?
不能,因为没有缺省的强转运算符,我们需要自己写:

operator int() const
{
	return value;
}
b = (int)a;
//b = a.operator int();
//b = operator int(&a);

同时,还要注意,强转运算符的重载不需要返回类型,其返回的类型,其实就是其强转的类型
上述强转运算符的重载的好处就是,方便了对象和对象,以及对象和变量之间的大小比较,无需重写<>运算符,否则就需要写一系列繁杂的比较函数,代码及其不简洁。

为什么我们要用列表方式初始化对象?

先来看下面的一段程序,猜测其运行结果:

class Int
{
private:
    int value;
public:
    Int(int x = 0) :value(x) { cout << "Create Int: " << this << endl; }

    Int(const Int& it) :value(it.value) { cout << "Copy Create Int: " << this << endl; }

    Int& operator=(const Int& it)
    {
        if (this != &it)
        {
            value = it.value;
        }
        cout << this << " = " << &it << endl;
        return *this;
    }
    ~Int() { cout << "Destroy Int : " << this << endl; }
};
class Object
{
	int num;
	Int val;
public:
	Object(int x,int y)
	{
		cout << "Create Object: " << this << endl;
		num = x;
		val = y;
	}
	~Object()
	{
		cout << "Destroy Object : " << this << endl;
	}
};
int main()
{
	Object obj(1,2);
}

分析其执行逻辑:

  1. 进入主函数,系统为obj对象分配空间,但还未构建对象
  2. 进入到obj构造函数的构造函数体之前,构建其成员num和val
  3. 调动val的缺省构造函数
  4. 完成了obj对象的成员构建
  5. 进入到obj构造函数的内部,将x赋值给num,将y的值赋值给val
  6. 由于y是一个内置类型,隐式转换为Int后调动构造函数创建无名的临时对象
  7. 调动赋值语句
  8. 析构临时对象
  9. 析构obj
  10. 析构val

在这里插入图片描述

注意看:先创建成员对象,再创建自身,然后先析构自身,再析构成员对象,进入到构造函数体之前,先构建自身的成员对象。
然而,若修改构造函数为如下的列表形式,运行状况就会发生一定的变化。

Object(int x,int y):num(x),val(y)
	{
		cout << "Create Object: " << this << endl;
	}

对于内置类型,虽然顺序不同,但效果相同,进入到构造函数体之前,就对num进行构建。
但对于自定义类型,到达构造函数,就依次构建num和val,不再产生临时对象

在这里插入图片描述
上述方式相当于对之前的优化,因此,对类的成员我们尽可能地应该使用列表方式构建,因为产生的对象最少,成本最低。

类中成员的构建顺序

按照设计的顺序,即我们声明的顺序
看下面的代码:

class Object
{
	int num;
	int sun;
public:
	Object(int x = 0):sum(x),num(sum){}

	void print() const 
	{
		cout<<num<<" "<<endl;
	}
};
int main()
{
	Object obj;
	return 0;
}

即便构造函数书写的列表顺序是先构建sum,再构建num,但是,其仍会根据声明顺序先构建num,由于sum还未进行构建,因此,num的值为随机值,而sum的值为0

析构函数

当一个对象定义时,C++自动调用构造函数建立该对象并进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数注销该对象并进行善后工作,这个特殊的成员函数即析构函数(destructor):先释放资源,再释放空间。

析构函数的特征如下:

  1. 析构函数的函数名和类名相同,但在前面需要加上~
  2. 析构函数无返回值类型,与构造函数在这一方面是一样的,(在有些编译器中会返回已销毁对象的地址),但析构函数不带任何参数
  3. 一个类有一个也只有一个析构函数,这与构造函数不同。
  4. 对象注销时,系统自动调动析构函数。
  5. 如果类中没有给出析构函数,则C++编译器自动给出一个缺省的析构函数。
    ——类名(){}

看一下下面的例子分析以下程序的执行结果:

class Object:
{
	int value;
public:
	Object(int x = 0):value(x)
	{
		cout<<"Create Object:"<<value<<endl;
	}
	~Object()
	{
		cout<<"Destroy Object:"<<value<<endl;
	}
};

void fun()
{
	Object a(1);
}

Objcet b(2);
int main()
{
	Object c(3);
	fun();
	return 0;
}

Object d(4);

分析:
对于该程序,首先会给两个全局对象b和d在.data域申请空间,并将对象创建在这两个空间内,(全局对象的构建并不会因为在主函数的前或后而受到影响——因为这是编译链接过程不是程序的执行过程)然后再进入主函数,在函数的栈空间中给对象c创建空间,然后进入fun函数,在fun函数的栈空间中创建空间,并构建对象a,然后fun函数结束,释放其所在的栈空间,因此就需要调用对象a的析构函数,之后主函数结束调动对象c的析构函数,最好,再释放d和b的空间,(与创建顺序相反(.data区的构建和析构过程和栈类似))。

因此执行结果如下:
在这里插入图片描述
从上面的例子中我们可以学习到:

  • 内置类型有空间即可操作,内置类型没有析构函数
  • 空间是和对象分离的
  • 全局对象的构建和析构由系统控制
    -对象可以调动析构函数完成自杀过程

在C++中,对于内置类型,为了结合面向对象的思想和C++的语法规则,在定义变量时有如下的几种写法:

int a = 10;
int b(10);
int c = int(10);//伪构造函数

int *p = new int(10);

使用析构函数实现内存的自动管理

class Object
{
	Int *ip;
public:
	Object(Int *s  NULL):ip(s)
	{}
	~Object()
	{
		if(ip != NULL)
		{
			delete ip;
		}
		ip = NULL;
	}
};
int fun()
{
	Object obj(new Int(10));
	//......
}
int main()
{
	fun()
}

上述程序只需要new对象,无需进行delete操作,因为其生存期结束后会自动调用析构。
然而,若使用下面的方法,就需要手动delete,否则会发生内存泄漏。

int fun()
{
	Int *ip = new Int(10);
	//...
	delete ip;
}
int main()
{
	fun();
	return 0;
}

new和malloc的区别

  1. 都在堆区开辟空间,若堆区空间不足失败,malloc返回一个空指针,new抛出异常throw bad_alloc
  2. malloc是C库函数只是开辟空间,但无对象,只有一个动作,必须通过定位new来构建对象
  3. new是标识符,关键字,运算符(可以重写)申请开辟空间,调动构造函数构建对象,两个动作。
  4. new会自动计算对象的大小

free和delete

  1. free释放空间
  2. delete调动指针指向的对象的析构函数释放资源,然后系统释放空间,注销对象(动态)
Object *op = (Pointer*)malloc(sizeof(Pointer));;
new(op) Object(10);

op->~Object();
free(op);
op = NULL;
Object *op = new Object(10);
delete op;
op = NULL:

new的三种调动形式

  1. 运算符用法(关键字调动)
    开辟空间,调动构造函数构建对象
Object *op = new Object(10);
//Object *op = NULL;
//op = new Object(10);
  1. 函数用法(函数调动)

    和malloc很像,此时不会调动构造函数,只申请空间
    但区别是:此时申请失败仍然抛出异常throw bad_alloc

Object *os = (Object*)::operator new(sizeof(Object));
  1. 定位new
 new(有效地址空间)

并不开辟空间,调动构造函数,os已经申请到空间,在os指定的空间中创建对象

new(os) Object(100);
os->~Object();

定位new可以完成对象的二次构建

new(&a) Object(10000);
//a.Object(10000);

new申请一个对象和一组对象的语法区别()[ ]

class Object{
	int value;
public:
	Object(int x = 0):value(x)
	{
		cout<<"Create Object:"<<value<<endl;
	}
	~Object()
	{
		cout<<"Destroy Object:"<<value<<endl;
	}
};

int main()
{
	int n;
	cin>>n;
	
	Object obja(20);//.stack——>函数开始调动构建,函数结束调动析构
	Object objb[10];//.stack——>调动10次构建,函数结束调动10次析构
	
	Object *s = new Object(n);//.heap
	Object *p = new Object[n];//.heap
	

	delete s;
	delete []p;//编译器会自动计算出销毁多少个对象(已经记录在案)
}

new关键字完成的操作:创建空间——>调动构造函数——>并将创建好的对象的地址返回
注意:

  • 圆括号,上述程序中创建了一个对象,,value的值为输入值n
  • 方括号,上述程序中创建了一组对象,value使用缺省值0——>也就是说动态构建一组对象,时,需要缺省构造函数(在C11标准中,支持构建一组对象并在方括号后用圆括号初始化)

问题:new和delete能混用吗?c++为什么区分单个元素和数组的内存分配和释放呢?

new delete
new[] delete []

①对于普通的编译器内置类型 ,new/delete[],new[]/delete混用没什么区别
②对于自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,会多开辟4个字节,用来记录对象的个数

请添加图片描述

引用

首先通过一个简单的例子学习一下底层是如何处理引用的?
引用就是自身为常性的指针

void fun(int &ap)
{
	int *p = &ap;
	ap = 100;
}
int main()
{
	int x = 10;
	int &p = x;
	p = 200;
	fun(x);
	fun(p);
	return 0;
}

经编译后:

void fun(int * const ap)
{
	int *p = ap;
	*ap = 100;
}
int main()
{
	int x = 10;
	int *const p = &x;
	*p = 200;
	fun(&x);
	fun(p);
	return 0;
}

从代码角度看,引用没有空间,只是一个别名
从机器角度看,引用有空间,它会开辟空间,其本质是一个指针。

什么时候可以返回对象或变量的地址和引用?

答:变量和对象的生存期不受函数影响时。

const常引用

int x = 0;
int &a = x;
a += 10;

const int &b  = x;

b可以引用a,但是b只能读取x的值不能通过b去改变x的值。

拷贝构造函数

同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员是共用的(只有一份拷贝)。在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝构造函数(Copy Constructor)。
 系统会提供一个缺省的构造函数。
 不可能把一个对象赋值给一个空间,因此拷贝构造函数仍然是一个构造函数,创建一个对象
拷贝构造函数的参数——同类型对象的引用。如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归

当拷贝构造函数的形参不加引用时,如下:

Objcet (const Object e)
{}
//没有引用相当于:Object obj2(obj1);
//拿实参obj1初始化形参e,就会调动拷贝构造函数,那么就会再拿e初始化,无穷递归下去。

void fun(Object obj){}

int main()
{
	Object obj1(10);
	fun(obj1);//实参与形参结合
}

拷贝构造函数的调用时机:

  1. 当函数的参数为类的对象时
#include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    CExample(int b)
    {
        a=b;
        printf("constructor is called\n");
    }
    CExample(const CExample & c)
    {
        a=c.a;
        printf("copy constructor is called\n");
    }
    ~CExample()
    {
     cout<<"destructor is called\n";
    }
    void Show()
    {
     cout<<a<<endl;
    }
};
void g_fun(CExample c)
{
    cout<<"g_func"<<endl;
}
int main()
{
    CExample A(100);
    CExample B=A;
    B.Show(); 
    g_fun(A);
    return 0;
}

调用g_fun()时,会产生以下几个重要步骤:
(1).A对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把A的值给C。 整个这两个步骤有点像:CExample C(A);
(3).等g_fun()执行完后, 析构掉 C 对象。

  1. 函数的返回值是类的对象
#include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    //构造函数
    CExample(int b)
    {
     a=b;
        printf("constructor is called\n");
    }
    //拷贝构造函数
    CExample(const CExample & c)
    {
     a=c.a;
        printf("copy constructor is called\n");
    }
    //析构函数
    ~CExample()
    {
     cout<<"destructor is called\n";
    }
    void Show()
    {
     cout<<a<<endl;
    }
};
CExample g_fun()
{
    CExample temp(0);
    return temp;
}
int main()
{
    
    g_fun();
    return 0;
}
 

当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1). 先会产生一个临时变量,就叫XXXX吧。
(2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3). 在函数执行到最后先析构temp局部变量。
(4). 等g_fun()执行完后再析构掉XXXX对象。

  1. 对象需要通过另外一个对象进行初始化
CExample A(100);
CExample B=A;
 

补充
一个类中如果没有定义任何函数,编译器则会自动默认添加(类中的六个缺省函数

  1. 构造函数
  2. 拷贝构造函数
  3. 赋值语句重载函数
  4. 析构函数
  5. 取地址符重载操作
  6. 常性取地址符的重载
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值