理论知识:C++构造函数与析构函数
关于函数有两个概念,函数的定义及函数的原型
//函数的原型
double sqrt(double);
//函数的定义
double sqrt(double x)
{
..........
}
总结一句话:构造函数就是初始化,析构函数就是释放空间
(以上的函数,均需要有函数的原型及定义)
目录
4成员初始化列表(member initializer list)语法
1关于构造函数(类的初始化)
- 当没有定义任何的构造函数时,编译器才会提供默认的构造函数
- 当定义构造函数后,务必定义默认构造函数,否则报错。当定义对象时,如果主动使用构造函数没有初始化,那程序会自动调用默认构造函数
- 构造函数不一定只有一个,但是有多个构造函数时,条件是每个函数的特征标各不同(构造函数支持函数重载)。
首先,说明如何声明及定义构造函数
同样的,构造函数也可以是private,当我们使用单例模式的时候,可以把类的构造函数声明成private的,这样就能保证外界不能实例多个对象出来了
构造函数书写要求:
- 与类名相同
- 无返回值
- 不写void
- 可以重载(可以有参数)
//原型—— 位置:类定义public中
//构造函数的原型,此处只是举例子,并不是所有的构造函数的参数,都需要使用按引用传递
Stock(const std::string & co,long n = 0,double pr = 0.0);
//函数的定义——位置:类声明的公有部分
Stock::Stock(const string & co,long n,double pr)
{
company = co;
if(n<0)
{
..............
}
..........
share_val = pr;
..........
}
1.1explicit
C++提供了关键字explicit(多数应用于只有一个参数的类构造函数),可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。(不能被隐式地调用,只能显式地调用)
https://blog.csdn.net/guodongxiaren/article/details/24455653
explicit使用注意事项:
- explicit 关键字只能用于类内部的构造函数声明上。
- explicit 关键字作用于单个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),如Circle(int x, int y = 0) 。
- 在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
- 修饰拷贝构造函数后,禁止隐式调用拷贝构造函数。
explicit构造函数是用来防止隐式转换的。请看下面的代码:
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Test1的构造函数带一个int型的参数,代码(1)处会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码(2)处会出现编译错误。
普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。
(来源:牛客网)
题目:
所以此处不能调用参数为int的构造函数。我认为此时编译器应该在所有带参构造函数中寻找适合的。因为上述中的n可以被转换为short,所以选择了参数是short的构造函数。 如果把参数short改为string,编译器就会报错,因为n转换不为string。
1.2默认构造函数(无参数)的原型及定义
stock00.h
class Stock
{
private:
std::string company;
long shares;
double share_cal;
public:
void acquire(const std::string & co,long n,double pr);
void buy(long num,double price);
void sell(long num,double price);
//函数声明中,参数可以相同,毕竟是按照参数传递而不是按值传递
//默认构造函数方式的原型
Stock();
}
stock00.cpp
..........
//默认构造函数的定义
Stock::Stock()
{
string company = "no name";
shares = 0;
share_cal = 0.0;
}
............
有些构造函数的原型中的参数也不一定非要是引用变量,下面这个可以作为参考
mytimee0.h
........
class Time
{
private:
int hourse;
int minutes;
public:
Time();
Time(int h,int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0,int m = 0);
Time sum(const Time & t) const;
void show() const;
}
那么关于以上内容,在定义完对应类的对象时,我们应该如何使用呢?
显式调用构造函数(有等号)
Stock food = Stock("World Cabbage",250,1.25);
隐式调用构造函数(只有括号)
Stock garmet("Furry Mason",50,2.5);
当然了,默认构造函数和构造函数都写了
那么就可以为所欲为的不进行调用
Stock frist;
Stock *pr = new Stock();
这个时候编译器会自动调用默认构造函数的
使用对象数组时,如何进行调用。
Stock my[4];//这就是对象数组
my[0].show();//show为Stock类中的公共接口
my[1].show();
//具体的调用过程
const int STKS = 4;
Stock stock[4] = {
Stock("NanoSmart",12.5,20);
Stock("Nanoscdasd",124.5,20);
Stock( );
};
一共四个值,但是只调用了三个,前两个使用了构造函数,第三个使用了默认构造函数,第四个未出现的内容必然也是使用了默认构造函数。
初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。
除了上述的定义默认构造函数的方法,还有一种方法
构造函数和默认构造函数二合一,一个函数原型一个函数定义
.........h
Stock(const std::string & co = “Error”,long n = 0,double pr = 0.0);//默认构造和构造函数一起
//以上形式使用了默认参数
.........cpp
Stock::Stock(const string & co,long n,double pr)
{
company = co;
if(n<0)
{
..............
}
..........
share_val = pr;
..........
}
目前来说,就以上内容,显示隐式调用构造函数还是和以前一样。调用默认构造函数就是就默认的值给相关的变量。
1.3构造函数和public&private
一般情况下,构造函数都是public,但是在单例模式下,比较特殊:
当我们想利用单例模式的时候,就可以把类的构造函数声明成private的,这样就能保证外界不能实例多个对象出来了。
下面看一个示例,关于构造函数的调用
题目1:
MyClass c1,*c2;
MyClass *c3=new MyClass;
MyClass &c4=c1;
c1要调用一次构造函数;
c2只是一个指针,用来动态描述对象,不会调用类的构造函数;
c3的右边新创建一个对象会调用构造函数。但是注意,这里的赋值运算符不是类中的赋值运算符,而是普通的赋值运算符;
c4是一个对象引用,是c1的一个别名,所以不会调用构造函数。
(题目和解答都来自牛客网,@huixieqingchun)
只要类的对象被创建,就会执行构造函数
c1,创建对象c1,调用了构造函数;
c2,声明了一个指向MyClass类型的指针,未调用构造函数;
c3,new MyClass在内存中创建了一个对象,并把对象地址赋给指针c3,创建对象调用了构造函数;
c4,将c4声明为引用,并将c1赋给它,即c4只是c1的一个引用,未调用构造函数。
2关于析构函数(类的释放)
- ~类名
- 无返回值
- 不写void
- 不可以重载(不可以有参数)
stock00.h
class Stock
{
private:
std::string company;
long shares;
double share_cal;
public:
void acquire(const std::string & co,long n,double pr);
void buy(long num,double price);
void sell(long num,double price);
//析构函数原型
~Shock();
}
stock00.cpp
//类声明
#include<iostream>
#include"stock.h"
//析构函数定义
Stock::~Stock()
{
.....;
//可以啥都没有
//但是使用new分配空间时,记得使用delete释放
//析构函数是和构造函数相对应的,当构造函数中使用new时,析构函数中需要使用delete进行删除
}
在类定义的过程中声明函数的原型,在类声明中进行函数的定义
- 如果创建的是静态存储类对象,则其析构函数就在程序完成代码时自动被调用
- 如何创建的是动态存储对象,则在其delete释放内存时,会被调用
- 如何创建的是自动存储对象,则在完成其代码段之后,会被调用
总结:关于析构函数是和构造函数是对应的,当类定义的对象要被释放时,就会调用相应的析构函数
我们下面看一个例子:
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?
C c;
void main()
{
A*pa=new A();
B b;
static D d;
delete pa;
}
其中全局变量和静态局部变量时从 静态存储区中划分的空间,
二者的区别在于作用域的不同,全局变量作用域大于静态局部变量(只用于声明它的函数中),
而之所以是先释放 D 在释放 C的原因是, 程序中首先调用的是 C的构造函数,然后调用的是 D 的构造函数,析构函数的调用与构造函数的调用顺序刚好相反。
局部变量A 是通过 new 从系统的堆空间中分配的,程序运行结束之后,系统是不会自动回收分配给它的空间的,需要程序员手动调用 delete 来释放。
局部变量 B 对象的空间来自于系统的栈空间,在该方法执行结束就会由系统自动通过调用析构方法将其空间释放。
之所以是 先 A 后 B 是因为,B 是在函数执行到 结尾 "}" 的时候才调用析构函数, 而语句 delete a ; 位于函数结尾 "}" 之前
答案:
(来自牛客网:@老鼠)
以上题目很好的解释了,静态对象,动态对象和自动存储类对象析构函数的调用时机。
示例:
stock00.h
//类定义
class Stock
{
private:
std::string company;
long shares;
double share_cal;
public:
void buy(long num,double price);
void sell(long num,double price);
void update(double price);
//默认构造函数的原型
Stock();
//构造函数的原型
Stock(const std::string & co,long n = 0,double pr = 0.0);
//析构函数的原型
~Shock();
}
stock00.cpp
//类声明
#include<iostream>
#include"stock00.h"
//默认构造函数的定义
Stock::Stock()
{
string company = "no name";
shares = 0;
share_cal = 0.0;
}
//构造函数的定义
Stock::Stock(const string & co,long n,double pr)
{
company = co;
if(n<0)
{
..............
}
..........
share_val = pr;
..........
}
//析构函数的定义
Stock::~Stock()
{
.....;
//可以啥都没有
//但是使用new分配空间时,记得使用delete释放
//析构函数是和构造函数相对应的,当构造函数中使用new时,析构函数中需要使用delete进行删除
}
//类中对应的公共接口的类对象,书写相应的功能即可
void Stock::buy(long num,double price)
{
............
}
void Stock::sell(long num,double price)
{
.............
}
void Stock::update(double price)
{
.............
}
usecjm.cpp//用户cpp
#include<iostream>
#include"stock00.h"
int main()
{
using std::name;
Stock stock1("Nam",12,20.0);//隐式调用构造函数
Stock stock2 = Stock ("Bono",2,2.0);//显式调用构造函数
stock1.updata();
stock2.updata();
}
多个构造函数的情况(函数重载):
stonewt.h
........
class Stonewt
{
private:
int stone;
double pds_left;
double pounds;
public:
Stonewt(double lbs);//构造函数1
Stonewt(int stn,double lbs);//构造函数2
Stonewt();//默认构造函数
~Stonewt();//析构函数
void show_lbs();
void show_stn();
};
#endif
上述内容有三个构造函数(两个构造函数一个默认构造函数)
分别是让用户定义一个浮点或两个浮点数,也可以直接创造Stonewt对象,而不进行初始化。
Stonewt blossem(132.5);//创建对象使用构造函数1
Stonewt buttercup(10,2);//创建对象使用构造函数2
Stonewt bubbles;//创建对象使用默认构造函数
3new和delete对构造函数与析构函数的影响
注意事项:
- 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete
- new和delete必须相互兼容,new对应用delete,而new 【】 对应于delete 【】;
- 如果含有多个构造函数,则必须以相同的方式使用new,要么都带【】,要么都没有!因为只有一个析构函数,所有的构造函数中的new必须和析构函数中的delete匹配。
构造函数
Person::Person():
Age(0),
name("")
{
qDebug()<<"进入构造函数";
}
Person::Person(int i)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
}
Person::Person(const int i,const QString C_name)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
name = C_name;
}
Person::Person(const Person & p)
{
qDebug()<<"const Person & p 进入构造函数";
Age = p.Age;
name = p.name;
}
Person::~Person()
{
qDebug()<<"进入析构函数";
}
cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//查隐式类型转换
test01();
test00();
}
void test00()
{
qDebug()<<"进入test00";
Person * maxtrix = new Person[10];//堆区只能调用默认构造函数,所以使用new的时候务必确保有默认构造函数在
Person max[3] = {Person(1),Person(1),Person(1)};//栈区可以调用其他类型的构造函数
delete [] maxtrix;
qDebug()<<"完成test00";
}
void test01()
{
qDebug()<<"进入test01";
Person * new_p = new Person;
delete new_p;
qDebug()<<"完成test01";
}
先看test00的输出
new为数组分配空间
类 * 变量名 = new 类[长度];
delete需要与之配套
delete [ ] 变量名;
在堆区,每次开辟一个空间,即会调用一次构造函数(且是默认构造函数),结束后即可释放。如maxtrix;
在栈区,可以选择其他带参数的构造函数。如max[]
再看test01的输出结果
test01中是给正常指针分配空间并delete
4成员初始化列表(member initializer list)语法
常规构造函数:
Quene::Quene(int qs)
{
front = rear = NULL;
item = 0;
qsize = qs;
}
改进一下:
Quene::Quene(int qs):qsize(qs),front(NULL),rear(NULL),item(0) {}
注意,只有构造函数可以使用这种初始化列表的方法。
GraphModifier::GraphModifier(Q3DBars *bargraph)
: m_graph(bargraph),
m_xRotation(0.0f),
m_yRotation(0.0f),
m_fontSize(30),
m_segments(4),
m_subSegments(3),
m_minval(-20.0f),
m_maxval(20.0f),
//! [1]
m_temperatureAxis(new QValue3DAxis),
m_yearAxis(new QCategory3DAxis),
m_monthAxis(new QCategory3DAxis),
m_primarySeries(new QBar3DSeries),
m_secondarySeries(new QBar3DSeries),
//! [1]
m_barMesh(QAbstract3DSeries::MeshBevelBar),
m_smooth(false)
{
//! [2]
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium);
m_graph->activeTheme()->setBackgroundEnabled(false);
m_graph->activeTheme()->setFont(QFont("Times New Roman", m_fontSize));
m_graph->activeTheme()->setLabelBackgroundEnabled(true);
m_graph->setMultiSeriesUniform(true);
//! [2]
m_months << "January" << "February" << "March" << "April" << "May" << "June" << "July" << "August" << "September" << "October" << "November" << "December";
m_years << "2006" << "2007" << "2008" << "2009" << "2010" << "2011" << "2012" << "2013";
这是Qt中的一段程序,写的是构造函数,初值可以使用初始化列表进行,部分的内容是在构造函数内部进行初始化的。
没事儿可以看看Qt的示例,类写的太漂亮了。
5拷贝构造函数
用来举例的类:
class Person : public QMainWindow
{
Q_OBJECT
public:
Person();
explicit Person(const int i);
Person(const int i,const QString C_name);
Person(const Person & p);
~Person();
int Age;
QString name;
static int a;
static void func();
};
Person::Person():
Age(0),
name("")
{
qDebug()<<"进入构造函数";
}
Person::Person(int i)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
}
Person::Person(const int i,const QString C_name)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
name = C_name;
}
Person::Person(const Person & p)
{
qDebug()<<"进入拷贝构造函数";
Age = p.Age;
name = p.name;
}
Person::~Person()
{
qDebug()<<"进入析构函数";
}
5.1拷贝构造函数形式
声明(函数原型)
Person(const Person & p);
定义
Person::Person(const Person & p)
{
qDebug()<<"进入拷贝构造函数";
Age = p.Age;
name = p.name;
}
拷贝构造函数主要用于用已经初始化的对象初始未初始化的变量。当然还有其他用途,可见5.2
C++标准中也规定,拷贝构造函数,不允许,按值传递,必须,按引用传递。
如果按值传递,参数需要在栈区开辟空间,赋值给参数,这个过程又会调用拷贝构造函数,形成递归,循环往复,直到崩溃。
5.2拷贝构造函数的调用时机
拷贝构造函数发生在对象还没有创建,需要创建时,以下两种情况统一起来,就是这句话。
比如1的情况,p3还不存在,需要用p2去创建
再比如2的情况,形参p1还不存在,需要用p1去创建
当然了,当对象已经存在的情况下:
Person p1;
p1 = p2;
此时并不会调用拷贝构造函数。
1:用已经初始化的对象初始未初始化的变量
Person p3 = Person(p2);//显式调用 拷贝构造函数
Person p4(2,"Test");
Person p5(p4);//隐式调用 拷贝构造函数
2:以值传递的方式给函数参数传值
qDebug()<<"进入Copy_test";
Copy_test(p5);
qDebug()<<"完成Copy_test";
void Copy_test(Person p1)
{
qDebug()<<"p.Age"<<p1.Age<<endl<<"p.Four"<<p1.name<<endl;
}
显然Copy_test 是按值传递,此时就会调用拷贝构造函数。
调用Copy_test函数,一开始会在栈区先分配空间给参数
调用拷贝构造函数,将p5的值拷贝给栈区的参数,之后在函数调用结束后,释放空间。输出内容如下:
3:按值返回也会调用拷贝构造函数
传参,尤其是类或者结构体,尽量使用引用 const Person & p,和Person & Copy_test()....
相关题目:
#include<iostream>
using namespace std;
class MyClass
{
public:
MyClass(int i = 0)
{
cout << i;
}
MyClass(const MyClass &x)
{
cout << 2;
}
MyClass &operator=(const MyClass &x)
{
cout << 3;
return *this;
}
~MyClass()
{
cout << 4;
}
};
int main()
{
MyClass obj1(1), obj2(2);
MyClass obj3 = obj1;
return 0;
}
答案:12244
解析1:@todd_nk(牛客网)
C MyClass obj3 = obj1;
obj3还不存在,所以调用拷贝构造函数输出2
如果obj3存在,obj3 = obj1,则调用复制运算符重载函数,输出3
6深浅拷贝
文章分享:
C++ -- 深浅拷贝 https://blog.csdn.net/xu1105775448/article/details/80546950
浅析C++中的深浅拷贝 https://blog.csdn.net/qq_39344902/article/details/79798297
7附录
默认构造函数,含参构造函数,拷贝构造函数,显式和隐式构造函数,成员初始化列表
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
};
class Person : public QMainWindow
{
Q_OBJECT
public:
Person();
Person(const int i);
explicit Person(const int i,const QString C_name);
Person(const Person & p);//拷贝构造函数
~Person();
int Age;
// int * Four;
QString name;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
Person p;//调用默认构造函数
qDebug()<<"p.Age"<<p.Age<<endl<<"p.Four"<<p.name<<endl;
//显示调用
Person p1 = Person();
qDebug()<<"p1.Age"<<p1.Age<<endl<<"p1.Four"<<p1.name<<endl;
Person p2 = Person(1,"cjm");
qDebug()<<"p2.Age"<<p2.Age<<endl<<"p2.Four"<<p2.name<<endl;
Person p3 = Person(p2);
qDebug()<<"p3.Age"<<p3.Age<<endl<<"p3.Four"<<p3.name<<endl;
//括号调用
Person p4(2,"Test");
qDebug()<<"p4.Age"<<p4.Age<<endl<<"p4.Four"<<p4.name<<endl;
Person p5(p4);
qDebug()<<"p5.Age"<<p5.Age<<endl<<"p5.Four"<<p5.name<<endl;
}
MainWindow::~MainWindow()
{
delete ui;
}
Person::Person():
Age(0),
name("")
{
qDebug()<<"进入构造函数";
}
Person::Person(int i)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
}
Person::Person(const int i,const QString C_name)
{
qDebug()<<"int i,QString C_name 进入构造函数";
Age = i;
name = C_name;
}
Person::Person(const Person & p)
{
qDebug()<<"const Person & p 进入构造函数";
Age = p.Age;
name = p.name;
}
Person::~Person()
{
qDebug()<<"进入析构函数";
}
拷贝构造函数,即用已有值的对象初始化另外一个对象,参数是类,所以用引用,const为常规操作