C++ 构造函数与析构函数,与成员初始化列表语法

理论知识:C++构造函数与析构函数

关于函数有两个概念,函数的定义及函数的原型

//函数的原型
double sqrt(double);
//函数的定义
double sqrt(double x)
{
   ..........
}

总结一句话:构造函数就是初始化,析构函数就是释放空间

(以上的函数,均需要有函数的原型及定义)

目录

1关于构造函数(类的初始化)

1.1explicit 

1.2默认构造函数(无参数)的原型及定义

 1.3构造函数和public&private

2关于析构函数(类的释放)

3new和delete对构造函数与析构函数的影响

4成员初始化列表(member initializer list)语法

5拷贝构造函数

5.1拷贝构造函数形式

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

6深浅拷贝

7附录


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使用注意事项:

  1. explicit 关键字只能用于类内部的构造函数声明上。
  2. explicit 关键字作用于单个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),如Circle(int x, int y = 0) 。
  3. 在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
  4. 修饰拷贝构造函数后,禁止隐式调用拷贝构造函数。

 

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进行删除
}

在类定义的过程中声明函数的原型,在类声明中进行函数的定义

  1. 如果创建的是静态存储类对象,则其析构函数就在程序完成代码时自动被调用
  2. 如何创建的是动态存储对象,则在其delete释放内存时,会被调用
  3. 如何创建的是自动存储对象,则在完成其代码段之后,会被调用

总结:关于析构函数是和构造函数是对应的,当类定义的对象要被释放时,就会调用相应的析构函数

我们下面看一个例子:

设已经有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对构造函数与析构函数的影响

注意事项:

  1. 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete
  2. new和delete必须相互兼容,new对应用delete,而new 【】 对应于delete 【】;
  3. 如果含有多个构造函数,则必须以相同的方式使用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为常规操作

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值