构造函数与析构函数

一、构造函数与析构函数

构造函数是特殊的成员函数

创建类类型的新对象,系统自动会调用构造函数

构造函数是为了保证对象的每个数据成员都被正确初始化


几点详细说明:

函数名和类名完全相同

不能定义构造函数的类型(返回类型),也不能使用void

通常情况下构造函数应声明为公有函数,否则它不能像其他成员函数那样被显式地调用

构造函数被声明为私有有特殊的用途。

构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数(重载)


一个由c/c++编译的程序占用的内存分为以下几个部分

 1、栈区(stack)―   由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) ―   一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表, 3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区  ―常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区―存放函数体的二进制代码。  



不带参数的构造函数

如果程序中未声明,则系统自动产生出一个默认构造函数

全局对象的构造先于main函数


析构函数可以显示调用(很少这么做  只在一些特殊的场合才这这样)

test.h

#ifndef _TEST_H_
#define _TEST_H_
 
class Test
{
public:
    Test();
    Test(int num);
    void Display();
    ~Test();
private:
    int num_;
};
#endif

test.cpp

#include"Test.h"
#include<iostream>
using namespace std;
 
Test::Test()
{
    num_ = 0;
    cout<<"initializeing Default"<<endl;
}
Test::Test(int num)
{
    num_ = num;
    cout<<"initializeing..."<<num_<<endl;
}
void Test::Display()
{
    cout<<num_<<endl;
}
 
Test::~Test()
{
    cout<<"Distroy"<<num_<<endl;
}

main.cpp

#include "Test.h"
#include <iostream>
using namespace std;
//int main(void)
//{
//  Test t;
//  t.Display();
//
//  Test t2(10);
//  t2.Display();
//
//  //不仅仅分配了内存  还调用了构造函数
//  Test *t3 = new Test(20);
//  t3->Display();
//
//  //不仅仅释放了内存 还调用了析构函数  并且析构函数不能够被重载,不带参数
//  //析构与构造的顺序相反
//  //t3是堆内存,必须由程序员自己释放
//  //t1与t2是栈内存,当花括号结束后,再释放对象
//  delete t3;
//
//  return 0;
//}
 
//全局对象会比Main函数先于构造
//
//Test t(10);
//int main()
//{
//  cout<<"Entering main"<<endl;
//  cout<<"Exiting main"<<endl;
//}
 
//析构函数与数组
//int main(void)
//{
//  Test t[2] = {10,20};
//  Test *t2 = new Test(2);
//  delete t2;
//
//  Test *t3 = new Test[2];
//  //必须要有[]  否则会出现运行时错误
//  delete[] t3;
//  return 0;
//}
 
//析构函数可以显示调用
int main(void)
{
    Test t;
    //会有风险  如果析构函数里面释放内存的话  这样会释放两次
    //因为Test对象在生命周期结束时候,会自动调用析构函数
    t.~Test();
    return 0;
}

二、转换构造函数赋值与初始化区别explicit

转换构造函数:单个参数的构造函数

作用:将其它类型转换为类类型


所以构造函数的作用:

1、初始化(普通构造函数的功能)   2、类型转化(转换构造函数)

#include "Test.h"
int main(void)
{
    Test t(10);     // 带一个参数的构造函数,充当的是普通构造函数的功能
    t = 20;         // 将20这个整数赋值给t对象
    // 1、调用转换构造函数将20这个整数转换成类类型 (生成一个临时对象)
    // 2、将临时对象赋值给t对象(调用的是=运算符  将调用默认的=运算符 如同系统提供默认构造函数)
    Test t2;   
}

上面程序的输出为:


临时对象赋值成功后直接销毁。可以看出:类的构造函数只有一个参数是非常危险的,

因为编译器可以使用这种构造函数把参数的类型隐式转换为类类型


赋值与初始化区别:

Test t = 10; // 等价于Test t(10);这里的=不是运算符,表示初始化。

注:是普通的构造函数  不会产生临时对象  表示初始化


t = 20; // 赋值操作  调用转换构造函数

Test& operator=(const Test& other);  系统默认做的操作是成员赋值

Test& Test::operator=(const Test& other)
{
	cout<<"Test::operator="<<endl;
	if (this == &other)
		return *this;

	num_ = other.num_;
	return *this;
}

所以相当于t.operator=(t2)

注:要分清赋值操作与初始化操作


只提供给类的构造函数使用的关键字。

编译器不会把声明为explicit的构造函数用于隐式转换,它只能在程序代码中显示创建对象

/*explicit */Test(int num);

所以Test t = 10 与 t = 20都无法通过

三、构造函数初始化列表、对象成员及其初始化、const成员、引用成员初始化

构造函数的执行分为两个阶段:

1初始化段

2普通计算段(赋值之类的运算)

举个例子:

Clock::Clock(int hour/* =0 */, int minute/* =0 */, int second/* =0 */) : hour_(hour),
minute_(minute), second_(second)//真正的初始化
{
//hour_ = hour;//普通计算段
//minute_ = minute;
//second_ = second;
cout<<"Clock::Clock"<<endl;
}


对象成员及其初始化:

类可以有普通数据成员,也可以有对象成员

先初始化对象成员,再初始化自身  析构的顺序刚好相反

//给container分配内存时,先给object分配内存

对象成员的构造顺序,只看对象成员的顺序,因为类要实例化,会先实例化内存中的数据成员,再构造成员函数

注:如果对象成员没有默认构造函数,那么在初始化列表中就必须给出

const成员、引用成员的初始化(由一般到特殊)

1、const成员必须在初始化列表进行初始化

2、因为常量分配内存后要马上进行初始化  而如果在函数体内进行初始化时,就是赋值操作,而不是初始化操作!

而引用成员的初始化也只能放在初始化列表之中

3、对象成员(对象所对应的类没有默认构造函数)的初始化,也只能在构造函数的初始化列表中进行

#include<iostream>
using namespace std;

class Object
{
public:
    Object(int num = 0):num_(num),kNum_(num),refNum(num)
    {
        cout<<"Object"<<num<<"..."<<endl;
    }
    ~Object()
    {
        cout<<"~Object"<<num_<<endl;
    }
private:
    int num_;
    const int kNum_;
    int &refNum;
};
int main()
{
    Object obj(10);
    return 0;
}

如果要每个对象所对应的值都是常量,该如何实现??只能用枚举来进行实现,这样子就不用全局变量来进行控制

注:不想让const仅限于对象内部,想让所有的对象都指向同一个常量

#include <iostream>
using namespace std;
 
// const成员的初始化只能在构造函数初始化列表中进行
// 引用成员的初始化也只能在构造函数初始化列表中进行
// 对象成员(对象所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行
class Object
{
public:
    enum E_TYPE
    {
        TYPE_A = 100,
        TYPE_B = 200
    };
public:
    Object(int num=0) : num_(num), kNum_(num), refNum_(num_)
    {
        //kNum_ = 100;
        //refNum_ = num_;
        cout<<"Object "<<num_<<" ..."<<endl;
    }
    ~Object()
    {
        cout<<"~Object "<<num_<<" ..."<<endl;
    }
 
    void DisplayKNum()
    {
        cout<<"kNum="<<kNum_<<endl;
    }
private:
    int num_;
    const int kNum_;
    int& refNum_;
};
 
int main(void)
{
    Object obj1(10);
    Object obj2(20);
    obj1.DisplayKNum();
    obj2.DisplayKNum();
 
    cout<<obj1.TYPE_A<<endl;
    cout<<obj2.TYPE_A<<endl;
    cout<<Object::TYPE_A<<endl;
 
 
    return 0;
}

四、拷贝构造函数

用一个对象来初始化同类的另一个对象 称为拷贝构造函数

Test t(10);

Test t2(t);//如果没有编写 系统为我们提供默认的拷贝构造函数


功能:使用一个已经存在的对象来初始化一个新的同一类型的对象

声明:只有一个参数并且参数为该类对象的引用

如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员


Test(const Test &other);//接收的参数应该是对象的引用

Test::Test(const Test &other):num_(other.num_)

{

}

推荐在初始化列表进行列表 如果在函数体内就是赋值而不是初始化


注:

Test t2 = t   

Test t2(t)等价于拷贝构造函数


如果拷贝构造函数中的形参没有用引用的话  会有什么问题呢??

这就涉及到函数初始化时将实参初始化形参的问题

如果不是用引用的话

test other = other,又要调用拷贝构造函数,然后会造成递归,无限循环,而用引用传递的话是不会分配内存的


拷贝构造函数调用的几种情况:

1、当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。理所当然也调用拷贝构造函数。

2、当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。


针对1:


TestFun2的形参是引用,就不会调用拷贝构造函数:



针对2:


临时对象是马上销毁的,如果是没人接收他的话!!!!所以初始化后直接销毁!

如果是 t = TestFun3(t)的话(是赋值,而不是初始化),赋值成功后,临时对象没有存在的价值,临时对象还是会销毁的

总结:临时对象销毁:1、没人接管  2、赋值成功


如果是初始化操作的话

Test t2 = TestFun3(t);临时对象是不会销毁的(不是赋值,而是初始化)

这个比较特殊,是将临时对象改个名字,



临时对象被引用了,不能销毁它,不然就是一个无效的引用


总结:临时对象不被销毁:1、初始化改名  2、被引用



这时候不会调用拷贝构造函数了  还是原来的t!

但是这样子写会有些问题 必须使用const对象

const Test& TestFun4(const Test& t)

{

    //return const_cast<Test&>(t);  

    return t;

}

Test t2 = TestFun4(t);  //这不是无名临时对象变成有名,这是初始化操作,在这一步调用了拷贝构造函数

#include "Test.h"
#include <iostream>
using namespace std;
 
void TestFun(const Test t)
{
}
 
void TestFun2(const Test& t)
{
}
 
Test TestFun3(const Test& t)
{
    return t;
}
 
const Test& TestFun4(const Test& t)
{
    //return const_cast<Test&>(t);
    return t;
}
 
int main(void)
{
    Test t(10);
    //TestFun(t);
    //TestFun2(t);
 
    //t = TestFun3(t);
    //Test t2 = TestFun3(t);//无名对象改名
 
    //Test& t2 = TestFun3(t);
 
    //Test t2 = TestFun4(t);
    const Test& t2 = TestFun4(t);//初始化操作
    cout<<"........"<<endl;
 
    return 0;
}

五、深拷贝与浅拷贝、赋值操作、禁止拷贝、空类默认产生的成员

注:strncpy会比strcpy更安全些


	String s1("AAA");
	s1.Display();
	String s2 = s1;		// 调用拷贝构造函数
			        // 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_

此时会出现错误  因为调用了默认的拷贝构造函数  实施的是浅拷贝  等价于s2.str_ = s1.str_;等价于s2与s1指向同一块内存,但是当生存周期结束后,会删除两次内存,所以要提供拷贝构造函数,实现深拷贝,

char* String::AllocAndCpy(char* str)
{
	int len = strlen(str) + 1;
	char* tmp = new char[len];
	memset(tmp, 0, len);
	strcpy(tmp, str);
	return tmp;
}

String::String(const String& other)
{
	str_ = AllocAndCpy(other.str_);
}

如果是要指向同一块内存,要在析构函数做一些处理


深拷贝后还是会有错误!!这就是赋值操作

	String s3;
	s3.Display();
	s3 = s2;			// 调用等号运算符
					// 系统提供的默认等号运算符实施的是浅拷贝 s3.str_ = s2.str_;
					// s3.operator=(s2);

//赋值操作后实施的还是浅拷贝  还是会被删除  所以要提供等号运算符

String& String::operator =(const String &other)
{
	if (this == &other)
		return *this;
	//会产生一个对象  所以要先进行销毁
	delete[] str_;
	str_ = AllocAndCpy(other.str_);
	return *this;
}


禁止拷贝!!! 有些对象是独一无二的  是禁止拷贝给其他对象的,

只需要放在私有,并且不提供它的实现,这样子就无法编译过!!

不要推到运行时刻,能在编译时刻处理就在编译时刻处理

要让对象是独一无二的,我们要禁止拷贝,方法是将拷贝函数与=运算符声明为私有,并且不提供他们的实现


空类默认产生的成员

class Empty {};


Empty(); // 默认构造函数

Empty( const Empty& ); // 默认拷贝构造函数

~Empty(); // 默认析构函数

Empty& operator=( const Empty& );  // 默认赋值运算符

Empty* operator&();               // 取址运算符

const Empty* operator&() const;    // 取址运算符 const


在函数后面+const表示:

在这个函数体内不能修改参数的值,只能读取,你若参数赋值,则编译通不过,实际调用时实参的值不变

空类的大小是1个字节,,因为假如没有一个字节的话,如何实例化一个对象呢,


附录代码:

string.h

#ifndef _STRING_H_
#define _STRING_H_

class String
{
public:
	String(char* str="");
	~String();
	String(const String& other);
	String& operator=(const String& other);

	

	void Display();

private:
	char* AllocAndCpy(char* str);
	
	char* str_;
};

#endif // _STRING_H_

string.cpp

#include "String.h"
//#include <string.h>
#include <cstring>
#include <iostream>
using namespace std;

String::String(char* str/* = */)
{
	str_ = AllocAndCpy(str);
}

String::~String()
{
	delete[] str_;
}

String::String(const String& other)
{
	str_ = AllocAndCpy(other.str_);
}

String& String::operator =(const String &other)
{
	if (this == &other)
		return *this;
	
	delete[] str_;
	str_ = AllocAndCpy(other.str_);
	return *this;
}

char* String::AllocAndCpy(char* str)
{
	int len = strlen(str) + 1;
	char* tmp = new char[len];
	memset(tmp, 0, len);
	strcpy(tmp, str);
	return tmp;
}

void String::Display()
{
	cout<<str_<<endl;
}

main.cpp

#include "String.h"

int main(void)
{
	String s1("AAA");
	s1.Display();
	String s2 = s1;		// 调用拷贝构造函数
				// 系统提供的默认拷贝构造函数实施的是浅拷贝 s2.str_ = s1.str_

	String s3;
	s3.Display();
	s3 = s2;		// 调用等号运算符
				// 系统提供的默认等号运算符实施的是浅拷贝 s3.str_ = s2.str_;
				// s3.operator=(s2);
				// 要让对象是独一无二的,我们要禁止拷贝
				// 方法是将拷贝构造函数与=运算符声明为私有,并且不提供它们的实现
	return 0;
}

empty.cpp

#include <iostream>
using namespace std;

class Empty
{
public:
	Empty* operator&()
	{
		cout<<"AAAA"<<endl;
		return this;
	}

	const Empty* operator&() const
	{
		cout<<"BBBB"<<endl;
		return this;
	}
};

int main(void)
{
	Empty e;
	Empty* p = &e;		// 等价于e.operator&();

	const Empty e2;
	const Empty* p2 = &e2;

	cout<<sizeof(Empty)<<endl;

	return 0;
}


















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值