C++对象的构造和销毁

目录

对象的构造

对象的销毁

继承中的构造与析构

赋值与拷贝构造函数

有意思的补充

神秘的临时对象


对象的构造

成员变量的初始值

从程序设计的角度,对象只是变量,因此: 在栈上创建对象时,成员变量初始为随机值 ,在堆上创建对象时,成员变量初始为随机值 ,在静态存储区创建对象时,成员变量初始为0值 

int main()
{
    int* p = new int;
    int* pi = new int(); // 初始化为0

    cout << *p << endl;   // 垃圾数据
    cout << *pi << endl;  // 0

    return 0;
}

对象的初始化

在类中提供—个public的initialize函数 ,对象创建后立即调用initialize函数进行初始化 ,但initialize只是—个普通函数,必须显式调用 

C++中可以定义与类名相同的特殊成员函数 ,这种特殊的成员函数叫做构造函数 ,构造函数没有任何返回类型的声明 ,构造函数在对象定义时自动被调用          

对象定义和对象声明不同 

    -对象定义:申请对象的空间并调用构造函数 (Test t; // 定义对象并调用构造函数)

    -对象声明:告诉编译器存在这样—个对象 (extern Test t; // 告诉编译器存在名为t的Test对象)

带有参数的构造函数 

构造函数可以根据需要定义参数 ,一个类中可以存在多个重载的构造函数 ,构造函数的重载遵循C++重载 的规则

无参构造函数:没有参数的构造函数 

    -当类中没有定义构造函数时,编译器默认提供—个无参构造函数,并且其函数体为空 

拷贝构造函数:参数类型为const class_name&的构造函数

    -当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制

    -即当类中定义了拷贝构造函数,就不会提供默认的无参构造函数

一般情况下,构造函数在对象定义时被自动调用 ,一些特殊情况下,需要手工调用构造函数 

// test.cpp
#include <stdio.h>

class Test
{
public:
    Test()   // 当提供其他构造而不提供无参构造,编译器报错
    {
        printf("Test()\n");
    }
    Test(const Test&) // 不写默认提供
    {
        printf("Test(const Test& t)\n");
    }
    Test(int v)
    {
        printf("Test(int v), v = %d\n", v);
    }
};

int main()
{
    Test t;      // 调用 Test()
    Test t1(1);  // 调用 Test(int v)  初始化
    Test t2 = 2; // 调用 Test(int v)  初始化
    Test t3 = t2;// 调用 Test(const Test&)  初始化

    t = t2;      // 赋值
    
    Test ta[3] = {Test(), Test(1), Test(2)};  // 创建—个对象数组      
    Test t4 = Test(100);  

    int ii = 1; // 初始化
    ii = 10;    // 赋值
    int i(100); // 初始化

    // 注意C++中对象初始化和赋值差距
    // 初始化:对正在创建的对象进行初值设置 ,要调用构造函数
    // 赋值:对已经存在的对象进行值设置

    return 0;
}

浅拷贝 :拷贝后对象的物理状态相同 ,编译器提供的拷贝构造函数只进行浅拷贝! 

深拷贝 :拷贝后对象的逻辑状态相同 

需要深拷贝时机:对象中有成员指代了系统中的资源 ,成员指向了动态内存空间 ,成员打开了外存中的文件 ,成员使用了系统中的网络端口 ,...

自定义拷贝构造函数,必然需要实现深拷贝!

#include <stdio.h>

class Test
{
private:
    int i;
    int j;
    int* p;
public:
    int getI()
    {
        return i;
    }
    int getJ()
    {
        return j;
    }
    int* getP()
    {
        return p;
    }
    Test(int v)
    {
        i = 1;
        j = 2;
        p = new int;    // 成员变量指向堆空间(即成员变量保存堆空间地址)
        *p = v;
     }
    Test(const Test& t)
    {
        i = t.i;
        j = t.j;
        p = new int;   // 指向新的堆空间
        *p = *t.p;     // 拷贝值
    }
    void free()
    {
        delete p;
    }
};

int main()
{
    Test t1(3);
    Test t2(t1);

    printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP());
    printf("t1.i = %d, t1.j = %d, *t1.p = %d\n", t1.getI(), t1.getJ(), *t1.getP());

    printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP());
    printf("t2.i = %d, t2.j = %d, *t2.p = %d\n", t2.getI(), t2.getJ(), *t2.getP());

    t1.free();
    t2.free();

   // 如果调用默认拷贝构造函数,成员变量值一样,两个对象的p成员都指向同一块堆空间,但当释放堆空间时,两次释放了堆空间的内存

    return 0;
}

初始化列表的使用

C++中提供了初始化列表对成员变量进行初始化 

成员的初始化顺序与成员的声明顺序相同 ,成员的初始化顺序与初始化列表中的位置无关 ,初始化列表先于构造函数的函数体执行 

const成员变量,引用数据成员都要使用初始化列表初始化

#include <stdio.h>

class Value
{
private:
    int mi;
    const int mj;
    int& mk;
public:
    Value(int i) : mj(1), mk(mi)
    {
        mi = i;
        printf("mi = %d\n", mi);
    }
};

class Test
{
private:
    Value m2;
    Value m3;
    Value m1;  // 声明顺序 2 3 1
public:
    Test() : m1(1), m2(2), m3(3)  // 初始化顺序 2 3 1
    {
        printf("Test::Test()\n");
    }
};

int main()
{
    Test t;

    return 0;
}

局部、全局、堆对象的构造顺序

局部对象的构造顺序依赖于程序的执行流 ,堆对象的构造顺序依赖于new的使用顺序 ,全局对象的构造顺序是不确定的 

① 对于局部对象:当程序执行流到达对象的定义语句时进行构造 

② 对于堆对象 :当程序执行流到达new语句时创建对象 ,使用new创建对象将自动触发构造函数的调用

③ 对于全局对象 :对象的构造顺序是不确定的 ,不同的编译器使用不同的规则确定构造顺序 

test.h

#ifndef _TEST_H_  
#define _TEST_H_  
  
#include <stdio.h>  
  
class Test  
{  
public:  
    Test(const char* s)  
    {  
        printf("%s\n", s);  
    }  
};  
  
#endif  

main.cpp

#include "test.h"  
  
Test t4("t4");  
  
int main()  
{  
    Test t5("t5");  
}  

t1.cpp

#include "test.h"  
  
Test t1("t1");  

t2.cpp

#include "test.h"  
  
Test t2("t2");  

t3.cpp

#include "test.h"  
  
Test t3("t3"); 

gcc version 6.3.0 20170406 (Ubuntu 6.3.0-12ubuntu2)

    这也是现在几乎所有编译器构造全局对象的顺序了

gcc version 4.4.5 (Ubuntu/Linaro 4.4.4-14ubuntu5.1) 

对象的销毁

对象的清理

为每个类都提供—个public的free函数,对象不再需要时立即调用free函数进行清理, free只是—个普通的函数,必须显示的调用 ,对象销毁前没有做清理,很可能造成资源泄漏 

C++的类中可以定义—个特殊的清理函数 ,这个特殊的清理函数叫做析构函数 ,析构函数的功能与构造函数相反 ,定义: ~ClassName() 

析构函数是对象销毁时进行清理的特殊函数,析构函数没有参数也没有返回值类型声明 ,析构函数在对象销毁时自动被调用 ,析构函数是对象释放系统资源的保障

析构函数的定义准则 :当类中自定义了构造函数,并且构造函数中使用了系统资源(如:内存申请,文件打开,等), 则需要自定义析构函数。

构造与析构的调用顺序

单个对象创建时构造函数的调用顺序: 调用父类的构造过程,调用成员变量的构造函数(调用顺序与声明顺序相同) ,调用类自身的构造函数 

析构函数与对应构造函数的调用顺序相反,多个对象析构时:析构顺序与构造顺序相反

对于栈对象和全局对象,类似于入栈与出栈的顺序,最后构造的对象被最先析构,堆对象的析构发生在使用delete的时候,与delete的使用顺序相关!!

#include <stdio.h>  
  
class Member  
{  
    const char* ms;  
public:  
    Member(const char* s)  
    {  
        printf("Member(const char* s): %s\n", s);  
          
        ms = s;  
    }  
    ~Member()  
    {  
        printf("~Member(): %s\n", ms);  
    }  
};  
  
class Test  
{  
    Member mA;  
    Member mB;  
public:  
    Test() : mB("mB"), mA("mA")  
    {  
        printf("Test()\n");  
    }  
    ~Test()  
    {  
        printf("~Test()\n");  
    }  
};  
  
Member gA("gA");  
  
int main()  
{  
    Test t;  
      
    return 0;  
}  

                

 

继承中的构造与析构

子类对象的构造 

子类构造函数必须对继承而来的成员进行初始化 ,可以直接通过初始化列表或者赋值的方式进行初始化 ,也可以调用父类构造函数进行初始化 

父类构造函数在子类中的调用方式 

默认调用 :适用于无参构造函数和使用默认参数的构造函数 

显式调用 :1. 通过初始化列表进行调用 ,适用于所有父类构造函数,2. 子类构造函数体内显式调用父类有参构造

构造规则 

子类对象在创建时会首先调用父类的构造函数 ,先执行父类构造函数再执行子类的构造函数 ,父类构造函数可以被隐式调用或者显示调用

对象创建时构造函数的调用顺序 

调用父类的构造函数 ,调用成员变量的构造函数 ,调用类自身的构造函数 ( 先父母,后客人,再自己)

子类对象的析构

执行自身的析构函数 ,执行成员变量的析构函数 , 执行父类的析构函数 

#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
    Parent()
    {
        cout << "Parent()" << endl;
    }
    Parent(string s)
    {
        cout << "Parent(string s) : " << s << endl;
    }
};

class Child : public Parent
{
public:
    Child()/*隐式调用父类无参构造函数*/
    {
        cout << "Child()" << endl;
    }
    /*
    Child(string s)//隐式调用父类无参构造函数
    {
        cout << "Child(string s) : " << s << endl;
    }*/
    Child(string s) : Parent(s)/*通过初始化列表显式调用父类构造函数*/
    {
        cout << "Child(string s) : " << s << endl;
    }
};

int main()
{
    Child c;
    Child cc("cc");

    return 0;
}

 

#include <iostream>  
#include <string>  
  
using namespace std;  
  
class Object  
{  
    string ms;  
public:  
    Object(string s)  
    {  
        cout << "Object(string s) : " << s << endl;  
        ms = s;  
    }  
    ~Object()  
    {  
        cout << "~Object() : " << ms << endl;  
    }  
};  
  
class Parent : public Object  
{  
    string ms;  
public:  
    Parent() : Object("Default")  
    {  
        cout << "Parent()" << endl;  
        ms = "Default";  
    }  
    Parent(string s) : Object(s)  
    {  
        cout << "Parent(string s) : " << s << endl;  
        ms = s;  
    }  
    ~Parent()  
    {  
        cout << "~Parent() : " << ms << endl;  
    }  
};  
  
class Child : public Parent  
{  
    Object mO1;  
    Object mO2;  
    string ms;  
public:  
    Child() : mO1("Default 1"), mO2("Default 2")  
    {  
        cout << "Child()" << endl;  
        ms = "Default";  
    }  
    Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")  
    {  
        cout << "Child(string s) : " << s << endl;  
        ms = s;  
    }  
    ~Child()  
    {  
        cout << "~Child() " << ms << endl;  
    }  
};  
  
int main()  
{         
    Child cc("cc");  
      
    cout << endl;  
      
    return 0;  
}  

 

赋值与拷贝构造函数

编译器为每个类默认重载了赋值操作符 ,默认的赋值操作符仅完成浅拷贝 , 当需要进行深拷贝时必须重载赋值操作符 ,赋值操作符与拷贝构造函数有相同的存在意义

C++98中对于一个空类编译器默认提供的函数:

//对于class Test{};等价于下列写法
class Test
{
public:
    Test();//const Test t1;  构造函数
    Test(const Test& e);//Test t2(t1);  拷贝构造函数

    Test& operator = (const Test& e);//t2 = t1;  赋值运算符

    Test* operator & ();//Test* pt2 = &t2;  取址运算符(非const)

    const Test* operator & () const;//const Test* pt1 = &t1;  取址运算符(const)

    ~Test();//析构函数
};
#include <iostream>  
#include <string>  
  
using namespace std;  
  
class Test  
{  
    int* m_pointer;  
public:  
    Test()  
    {  
        m_pointer = NULL;  
    }  
    Test(int i)  
    {  
        m_pointer = new int(i);  
    }  
    Test(const Test& obj)  
    {  
        m_pointer = new int(*obj.m_pointer);  
    }  
    Test& operator = (const Test& obj)  
    {  
        if( this != &obj )  
        {  
            delete m_pointer;  
            m_pointer = new int(*obj.m_pointer);  
        }  
          
        return *this;  
    }  
    void print()  
    {  
        cout << "m_pointer = " << hex << m_pointer << endl;  
    }  
    ~Test()  
    {  
        delete m_pointer;  
    }  
};  
  
int main()  
{  
    Test t1 = 1;  //调用Test(int i)
    Test t2;  
    Test t3 = t1;//调用拷贝构造函数
      
    t2 = t1; //调用了该类的重载的赋值操作符
      
    t1.print();  
    t2.print();  
      
    return 0;  
}  

 —般性原则:重载赋值操作符,必然需要实现深拷贝!

有意思的补充

补充1

若父类没有显式的写出无参构造函数,子类不会调用父类的构造函数

#include <stdio.h>

class A
{
public :
	A() 
	{
		printf("我是A的构造函数\n");
	}
};

class B : public A
{
public:
	B()
	{
		printf("我是B的构造函数\n");
	}
};

int main()
{
	B b;

	return 0;
}

当删除A的构造函数

补充2

在初始化列表显式调用父类有参构造与在子类构造函数体显式调用父类有参构造完全等价

#include <stdio.h>

class A
{
public :
	A() {}
	A(int a, int b){}
};

class B : public A
{
public:
	B(int a, int b) : A(a, b)
	{
		this->A::A(a, b);
	}
};

int main()
{
	B b(1,2);

	return 0;
}

B构造函数体不可这样写

B(int a, int b) 
{
	A(a, b); //企图在B构造函数体中调用父类有参构进行初始化操作,实际生成了临时对象
}

理由如下

所以应该加上this作用域

补充3

推荐定义堆对象时加上括号

#include <iostream>
using namespace std;

class Test
{
public:
    int a;
};

int main()
{
    int* p = new int;         // 未初始化
    int* pi = new int();      // 初始化为0
    int* arr1 = new int[10];  // 未初始化
    int* arr2 = new int[10]();// 初始化为0

    Test* pt = new Test;
    Test* pti = new Test();

    cout << *p << endl;        // 垃圾数据
    cout << *pi << endl;       // 0
    cout << arr1[0] << endl;   // 垃圾数据
    cout << arr2[0] << endl;   // 0
    cout << (*pt).a << endl;   // 垃圾数据
    cout << (*pti).a << endl;  // 0

    // delete ...

    return 0;
}

 

 

 

神秘的临时对象

直接调用构造函数将产生—个临时对象,临时对象的生命周期只有—条语句的时间 ,临时对象的作用域只在—条语句中,临时对象是C++中值得警惕的灰色地带

临时对象是性能的瓶颈,也是bug的来源之— ,实际工程开发中需要人为的避开临时对象

现代C++编译器在不影响最终执行结果的前提下,会尽力减少临时对象的产生!!!

#include <stdio.h>  
  
class Test  
{  
    int mi;  
public:  
    Test(int i)  
    {  
        printf("Test(int i) : %d\n", i);  
        mi = i;  
    }  
    Test(const Test& t)  
    {  
        printf("Test(const Test& t) : %d\n", t.mi);  
        mi = t.mi;  
    }  
    Test()  
    {  
        printf("Test()\n");  
        mi = 0;  
    }  
    void print()  
    {  
        printf("mi = %d\n", mi);  
    }  
    ~Test()  
    {  
        printf("~Test()\n");  
    }  
};  
  
Test func()  
{  
    return Test(20);  
}  
  
int main()  
{                      //编译器的做法
    Test t = Test(10); // ==> Test t = 10;  
    Test tt = func();  // ==> Test tt = Test(20); ==> Test tt = 20;  
      
    t.print();  
    tt.print();  

    //Test().print();  
    //Test(10).print();  
      
    return 0;  
}  

    可以看到没有调用拷贝构造而是调用了有参构造

猜测:生成临时对象,用临时对象初始化t,调用拷贝构造函数

实际:编译器会尽量避开临时对象

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值