掘根宝典之C++类的构造函数和析构函数,默认构造函数,列表初始化

为什么要引入构造函数

我们先创建一个类AA

#include<iostream>
using namespace std;
class AA
{
private:
int a;
public:
void A()
{
cout<<a<<endl;
}
};
int main()
{
AA e;
}

C++的目标之一是让使用类对象就像使用标准类型一样,就像下面这样子使用

int a;//第一步
int b=9;//第二步
int c=b;//第三步

但是我们发现就上面的e对象只能实现第一步,不能给e赋值,也不能将另一个对象赋给e,也就是说,常规的初始化语法不适用于类型AA

#include<iostream>
using namespace std;
class AA
{
private:
	int a;
public:
	void A()
	{
		cout << a << endl;
	}
};
int main()
{
	AA e;
	AA w = { 6 };//这是不行的
	AA g = e;//这是不行的
}

不能这么初始化的原因是因为数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员,只能通过公有函数来初始化

有人想说了啊,把数据部分设为公有的不就好了嘛!!我们先试试

#include<iostream>
using namespace std;
class AA
{
public:
	int a;

	void A()
	{
		cout << a << endl;
	}
};
int main()
{
	AA e;
	AA w = { 6 };//完全可以
	AA g = w;//可以
}

可以发现使数据成员变为公有的,就可以按刚才的方式进行初始化了

但是使数据成员变为公有违背了类的一个主要初衷:数据隐藏

为了使类对象能正常使用常规初始化语法,我们引入了构造函数

声明和定义构造函数

c++专门提供了一个特殊的成员函数-------类构造函数,专门用于构造新对象,将值赋给它们的数据成员。更准确的说,c++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义,名称与类名相同。

c++的构造函数的名称与它所在类的类名相同,且它有一个有趣的特征,虽然没有返回值,但没有被声明为void类型。实际上构造函数没有声明类型。

构造函数的结构如下:

它所在类的类名 (参数列表)
{
函数体
}

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a_)//注意没有返回类型,原型位于类声明的公有部分
	{
		a = a_;
	}
};
int main()
{

	AA w = { 6 };//完全可以
	AA g = w;//可以
}

我们也可以通过构造函数来初始化其中的私有数据了,注意没有返回类型,原型位于类声明的公有部分

成员名和参数名

不熟悉构造函数的你可能会试图将类成员名称用作构造函数的参数名

就像下面这样子

class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a)
	{
		....
	}
};

这是错误的。构造函数的参数表示的不是类成员,是赋给类成员的值。因此,参数名不能与类成员相同,否则会出现下面这样子的错误

class AA
{
private:
	int a;
public:
	//构造函数
	AA(int a)
	{
		a=a;//这种错误
	}
};

为了避免这种混乱,一种常见的作法是在数据成员名中使用m_前缀

class AA
{
private:
	int m_a;
public:
	//构造函数
	AA(int a)
	{
		m_a=a;
	}
};

另外一种就是在成员名后加_

class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_=a;
	}
};

使用构造函数

每次创建类对象(甚至使用new动态内存分配)时,c++都使用类构造函数

c++提供了两种使用构造函数来初始化对象的方式。

第一种是显式地调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w=AA(3);//显式调用构造函数
}

 

第二种是隐式调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w(3);//隐式调用构造函数
//和AA w=AA(3)是等价的
}

第三种,使用new

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
AA*e=new AA(3);
}

这创建一个AA对象,将其初始化为参数提供的值,并将该对象的地址赋给e指针。在这种情况下,对象没有名称,但可以使用指针来管理对象。

不能通过对象来调用构造函数

可以使用对象来调用一般函数,但是不能使用对象来调用构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)//注意没有返回类型,原型位于类声明的公有部分
	{
		a_ = a;
	}
};
int main()
{
 AA w(3);
w.AA(2);//这是不可以的
}

构造函数的其他用处

构造函数不仅仅可以用来初始化新对象,还可以用来赋值

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	
	AA(int a)
	{
		a_ = a;
	}
	AA()
	{}
};
int main()
{
	AA w;
	w = AA(2);//用构造函数给已有对象赋值

}

实际上下面这两种方式有根本性差别

AA w=AA(2);
w= AA(2);

 第一条是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会)

第二条语句是赋值,在赋值前总会导致在赋值前创建一个临时对象,再将临时对象复制到w里

所以我们尽量使用初始化语句 

C++11列表初始化

C++列表初始化允许用于类上!只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起。

AA a={1};
AA a{1};

默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。也就是说,它是用于下面这种声明的构造函数。

AA e;

这就有点像下面这种情况一样

int a;

默认构造函数有三种,分为隐式版本和显式版本,其中隐式版本有一种,显式版本有两种

隐式版本

如果在一个类里没有提供任何构造函数,则c++将自动提供默认构造函数,这就是构造函数的隐式版本,不做任何工作。

举个例子 

#include<iostream>
using namespace std;
class AA
{
private:
int a;
public:
void A()
{
cout<<a<<endl;
}
};
int main()
{
AA e;//c++自动提供默认构造函数
}

AA类没有提供任何构造函数, 则在调用AA e;时编译器将自动提供默认构造函数,这个c++自动提供的默认构造函数可能是下面这样子的;

AA()
{}
//什么都没有

c++自动提供的默认构造函数没有参数,因为声明中不包含任何值

显式版本

注意了:当且仅当没有定义任何构造函数时(注意注意),编译器才会自动提供默认构造函数

为类定义了非默认构造函数后(注意注意)如果我们还想用下面这种语句,程序员就必须为它提供显式的默认构造函数,否则会出错

AA w;

看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_ = a;
	}
};
int main()
{
 AA w;//编译器会报错
}

如果想像上面这么使用的话,就必须提供显式的默认构造函数

显式的默认构造函数有两种

第一种


给已有的构造函数的所有参数提供默认值;

如果对函数参数默认值不熟悉的可以看看这个:http://t.csdnimg.cn/vMUTT

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=4,int b=3)//注意是所有参数都有默认值
	{
		a_ = a;
        b_ = b;
	}
};
int main()
{
 AA w;
}

这没问题

如果不是所有参数都有默认值的话,那就不是默认构造函数

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
};
int main()
{
 AA w;//系统会报错
}

这就会出现问题

第二种

用函数重载来定义另一个构造函数——一个没有参数(函数体可以自己写点东西,也可以说明也不写)的构造函数

就像这个一样

AA()
{}

对函数重载不熟悉的可以看看这个:http://t.csdnimg.cn/341PH

举个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	//构造函数
	AA(int a)
	{
		a_ = a;
	}
    AA()
    {}
};
int main()
{
 AA w;//完全没问题
}

注意点

一个类里只能有一个显式的默认构造函数

也就是说,下面这种写法是错的

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=7,int b=3)//默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    AA()//默认构造函数
    {}
};

默认构造函数的函数体

默认构造函数的函数体可以为空,也可以自己写点东西进去

一般来说默认构造函数的函数体内容应该对所有的类成员进行初始化

就像下面这样子

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//非默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    AA()//默认构造函数
    {
       a_=7;
       b_=3;
     }
};

析构函数

为什么要引入析构函数

用构造函数创建对象以后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,也就是析构函数。用来完成清理工作。

我们先看看析构函数把!

析构函数和构造函数很像,它的名称很特殊:在类名前加~。比如AA类的析构函数名叫~AA

和析构函数一样,析构函数没有返回值和声明类型。

与构造函数不同的是,析构函数没有参数

也就是说AA类的析构函数原型应该是~AA()

我们看个例子

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a=7,int b=3)//默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    ~AA()//析构函数
    {}
};

析构函数的作用

析构函数的作用是在对象被销毁时执行清理操作。它主要用于释放对象所占用的资源,包括但不限于以下情况:

1.动态内存的释放:如果在对象的生命周期中使用了new运算符分配了内存,那么在析构函数中需要使用delete运算符释放这些内存,以免出现内存泄漏。

2.关闭文件或网络连接:如果在对象的生命周期中打开了文件或建立了网络连接,那么在析构函数中需要关闭文件或断开网络连接,以免资源泄漏。

3.释放其他资源:如果在对象的生命周期中使用了其他类型的资源,如数据库连接、锁等,那么在析构函数中需要释放这些资源,以确保系统能够正确运行。

4.执行额外的操作:在析构函数中还可以执行一些额外的操作,如保存对象的状态、触发某些事件等。

总之,析构函数的作用是释放对象所占用的资源,确保资源能够正确释放,防止资源泄漏,并且可以执行一些额外的操作。

最常用的就是第一点

例如构造函数用new来分配内存时,析构函数则用delete来释放内存。

如果说构造函数没有使用new,我们可以不写析构函数,这时编译器会自动生成一个什么都不做的隐式析构函数

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
    ~AA()
    {}
//因为构造函数没有使用new,所以可以什么都不写
};

构造函数不用new,那我们也可以不写析构函数,系统会自己提供一个什么都不做的析构函数

class AA
{
private:
	int a_;
    int b_
public:
	
	AA(int a,int b=3)//这不是默认构造函数
	{
		a_ = a;
        b_ = b;
	}
  
//因为构造函数没有使用new,所以可以什么都不写
};

调用析构函数

注意:对象消失时,编译器会自动调用析构函数,不必手动调用

我们可以看个例子

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
    ~AA()
    {
cout<<"析构函数被调用"<<endl;
    }
};
int main()
{
 AA w(2);
}

我们运行程序,发现什么也没打印,难道是析构函数不会被调用? 

实际上,当类对象被销毁时才会调用析构函数,上面这个例子编译器只会在mian()执行完毕后才会调用析构函数,如果我们想看到析构函数被调用,可以改进这个代码

#include<iostream>
using namespace std;
class AA
{
private:
	int a_;
public:
	AA(int a)
	{
		a_ = a;
	}
    ~AA()
    {
cout<<"析构函数被调用"<<endl;
    }
};
int main()
{
{
 AA w(2);
}
//w在此处已经被销毁
return 0;
}

这时我们就能看到析构函数被调用了 

总结

定义类时一般来说一定要写构造函数和析构函数,防止一切不确定的错误发生

最近更新

  1. TCP协议是安全的吗?

    2024-02-13 13:38:01       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-13 13:38:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-13 13:38:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-13 13:38:01       18 阅读

热门阅读

  1. VUE SEO 几种方案经典面试题

    2024-02-13 13:38:01       28 阅读
  2. 如何控制系统安全 或 控制流氓软件

    2024-02-13 13:38:01       26 阅读
  3. Python开发者转型Go开发

    2024-02-13 13:38:01       23 阅读
  4. SpringBoot中全局异常处理

    2024-02-13 13:38:01       28 阅读
  5. 跟我学C++中级篇——函数模板的匹配

    2024-02-13 13:38:01       32 阅读
  6. uniapp禁止截屏录屏/投屏功能(adb投放失效)

    2024-02-13 13:38:01       28 阅读
  7. XGboost和lightGBM算法对比

    2024-02-13 13:38:01       24 阅读