第十一章 类和对象——第2节 对象的初始化和清理

11.2 对象的初始化和清理

​ 对象的 初始化清理 是两个非常重要的安全问题

11.2.1 构造函数和析构函数

​ C++中利用构造函数和析构函数解决上述的安全问题,有以下特性:

​ (1) 两个函数被编译器自动调用,以完成对象初始化和清理工作

​ (2) 若用户不提供构造、析构函数,编译器会提供两个函数的空实现,因为初始化和清理是编译器强制用户做的事情

  • 构造函数:用于创建对象时为对象的成员属性赋值
  • 析构函数:用于对象销毁前执行清理工作

构造函数语法类名(){}

​ (1) 构造函数没有返回值,也不写void

​ (2) 函数名称与类名相同

​ (3) 构造函数可以有参数,可以发生重载

​ (4) 程序在调用对象时会自动调用,且仅调用一次

实际上就是一个 和类同名的不写返回值类型的被自动调用的类中的 一个成员函数

析构函数语法~类名(){}

​ (1) 析构函数没有返回值,也不写void

​ (2) 函数名称与类名相同,在名称前加 ~

​ (3) 构造函数不可以有参数,不可以发生重载

​ (4) 程序在对象销毁前会自动调用,且仅调用一次

实际上就是一个 ~类名 为函数名的不写返回值类型的被自动调用的类中的 一个成员函数

实例:

class Person {			// Person类创建
public:
	// 1.构造函数:进行初始化操作
	Person() {
		cout << "Person 构造函数 的调用" << endl;
	}

	// 2.析构函数:进行清理操作
	~Person() {
		cout << "Person 析构函数 的调用" << endl;
	}
};

void test1() {			// 测试函数
	Person p_local;		// 在函数中创建了一个Person类的实例p_local
}

int main() {			// 主函数
	test1();			// 调用测试函数
	Person p_global;	// 在主函数中创建了一个Person类的实力p_global

	system("pause");
	return 0;
}

/************** 结果显示 **************/
Person 构造函数 的调用
Person 析构函数 的调用
Person 构造函数 的调用

解释:

  • 主函数中调用了函数 test1() ,在该函数中创建了一个实例 p_local ,由于 p_local 是函数的局部变量,存放于栈区,栈区中的局部变量会随着函数调用结束而被销毁,故结果显示调用了Person类的构造函数和析构函数

  • 主函数中创建的实例 p_global ,由于 p_global 是全局变量,存放于全局区,在程序结束之后由编译器自动释放,故结果只显示调用了Person类的构造函数(因为还没有结束程序)

11.2.2 构造函数的分类与调用

(一)两种分类方式:

  • 按参数分:有参构造无参构造(也叫默认构造)

    class Person{
    public:
        Person(){
            // 无参构造
        }
        Person(int a){
            // 有参构造
        }
    }
    
  • 按类型分:普通构造拷贝构造

    class Person{
    public:
        Person(int a){
            age = a;
            // 普通构造
        }
        Person(const Person &p){
            // 拷贝构造
            age = p.age;	// 拷贝构造后可写类似于这样的代码进行属性拷贝
        }
    }	
    

(二)三种调用方式:

  • 括号法

    class Person{
    public:
        Person(){					// 无参构造
        }
        Person(int a){				// 有参构造
            age = a;
        }
        Person(const Person &p){	// 拷贝构造
            age = p.age;
        }
    }
    
    int main(){
        Person p1;		// 括号法调用默认构造函数(无参构造函数)
        Person p2(10);	// 括号法调用有参构造函数
        Person p3(p2); 	// 括号法调用拷贝构造函数
        
        cout << "p2的年龄为:" << p2.age << endl;
        cout << "p3的年龄为:" << p3.age << endl;
        
        system("pause");
        return 0;
    }
    

    注意:在调用默认构造函数时,无需加括号,否则编译器会认为是一个“返回值为Person,名称为p1的函数声明”,不认为在创建对象

  • 显示法

    class Person{
    public:
        Person(){					// 无参构造
        }
        Person(int a){				// 有参构造
            age = a;
        }
        Person(const Person &p){	// 拷贝构造
            age = p.age;
        }
    }
    
    int main(){
        Person p1;					// 显示法调用默认构造函数(无参构造函数)
        Person p2 = Person(10);		// 显示法调用有参构造函数
        Person p3 = Person(p2); 	// 显示法调用拷贝构造函数
        
        cout << "p2的年龄为:" << p2.age << endl;
        cout << "p3的年龄为:" << p3.age << endl;
        
        system("pause");
        return 0;
    }
    

    解释Person(10) 作为右值时称为 “匿名对象” ,p2 = Person(10) 表示将p2作为该匿名对象的名称,在当前行执行结束后,系统会自动回收该匿名对象(不会收p2)

    注意:不要利用拷贝函数来初始化匿名对象,即类似于Person(p3),其中p3为拷贝构造函数,因为编译器会认为Person (p3) == Person p3

  • 隐式转换法

    class Person{
    public:
        Person(){					// 隐式转换法无参构造
        }
        Person(int a){				// 隐式转换法有参构造
            age = a;
        }
        Person(const Person &p){	// 隐式转换法拷贝构造
            age = p.age;
        }
    }
    
    int main(){
        Person p1;			// 调用默认构造函数(无参构造函数)
        Person p2 = 10;		// 调用有参构造函数
        Person p3 = p2; 	// 调用拷贝构造函数
        
        cout << "p2的年龄为:" << p2.age << endl;
        cout << "p3的年龄为:" << p3.age << endl;
        
        system("pause");
        return 0;
    }
    
11.2.3 拷贝构造函数的调用时机

C++中拷贝构造函数的调用时机通常有以下三种情况:(以下方Person类为例)

class Person {
private:
	int m_age;
public:	
	Person() {					// 默认构造/无参构造
		cout << "Person默认构造函数的调用" << endl;
	}
	Person(int age) {			// 参数构造
		m_age = age;
		cout << "Person有参构造函数的调用" << endl;
	}
	Person(const Person& p) {	// 拷贝构造
		m_age = p.m_age;
		cout << "Person拷贝构造函数的调用" << endl;
	}
	~Person() {					// 析构函数
		cout << "Person析构函数的调用" << endl;
	}
};
  • 使用一个已经创建完毕的对象来初始化一个新对象

    void test01() {
    	Person p1(20);
    	Person p2(p1);
    }
    
  • 值传递的方式给函数参数传值

    void func(Person p) {
    }
    void test02() {
    	Person p0;
    	func(p0);
    }
    

    解释:有某函数func(Person p),在函数test02()中以值传递方式向func()传入参数p0,由于是值传递,实际上是系统拷贝p0的副本作为参数传入func(),对p0拷贝过程中会调用拷贝构造函数

  • 以值方式返回局部对象

    Person func2() {
    	Person p1;
        cout << (int*)&p1 << endl;
    	return Person(p1);
    }
    void test03() {
    	Person p2 = func2();
        cout << (int*)&p2 << endl;
    }
    

    解释

    1. 在主函数中调用test03()函数,创建了一个实例p2用于接收func()返回的p1,由于使用的是值返回,所以系统自动拷贝了一份p1用于返回,在这过程中调用了一次拷贝构造函数;
    2. 为什么在返回值使用Person(p1)而非之间使用p1:在C++11及以上版本中,引入了“移动语义”,即当实例p1作为右值时,编译器利用移动构造函数来避免不必要的深拷贝,这样会避免一次拷贝工作;此处为体现“值方式返回局部对象”时的拷贝行为,特意使用11.2.2中的显示法强制对返回值进行拷贝,从而调用一次拷贝构造函数
    3. cout << (int*)&p1 << endl;cout << (int*)&p2 << endl;分别对p1和p2的地址进行了打印,结果为:当返回值为Person(p1)时,两地址不同当返回值为p1时,两地址相同。原因如下:
      • 返回值为Person(p1)时,编译器执行流程为:
        • func()栈上创建一个局部对象p1
        • return Person(p1)执行时,编译器创建一个临时对象,使用拷贝构造函数将p1的内容拷贝至该临时变量中
        • 返回该临时变量
      • 返回值为p1时,编译器执行流程为:
        • func()栈上创建一个局部对象p1
        • return p;执行时,编译器不会创建临时对象,而是直接返回已经在栈上构造好的对象p1
11.2.4 构造函数的调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 若用户定义 有参构造函数 ,则C++不再提供 默认构造函数 ,但会提供 默认拷贝构造函数
  • 若用户定义 拷贝构造函数 ,则C++不再提高 其他构造函数(包含默认构造、有参构造、默认拷贝构造三种构造函数
11.2.5 浅拷贝与深拷贝

1.定义:

  • 浅拷贝:简单的复制拷贝

  • 深拷贝:在堆区申请空间进行拷贝

2.实例:

class Person {
	
public:
	int m_age;
	int* m_height;

	Person() {			// 无参构造
		cout << "Person类的无参构造函数调用" << endl;
	}
	Person(int age, int height) {
		m_age = age;
		m_height = new int(height);
		cout << "Person类的有参构造函数调用" << endl;
	}
	Person(const Person& p) {
		m_age = p.m_age;
		// m_height = p.m_height;
		// 深拷贝操作
		m_height = new int(*p.m_height);
		cout << "Person类的拷贝构造函数调用" << endl;
	}
	~Person() {			// 析构函数,将堆区开辟的数据进行释放
		if (m_height != 0) {
			delete m_height;
			m_height = NULL;
		}
		cout << "Person类的析构函数调用" << endl;
	}
};

void test01() {

	Person p1(20, 175);
	cout << "p1的身高为:" << *p1.m_height << endl;
	Person p2(p1);
	cout << "p2的身高为:" << *p2.m_height << endl;
}

int main() {

	test01();
	system("pause");
	return 0;
}

当拷贝构造函数Person(const Person& p){}未被定义时,系统会崩溃,原因如下:

  • 首先,Person类中m_height属性为int*型,是一个指针变量

  • test01()函数中调用Person p1(20,175),实际上是调用了有参构造函数,将20传给m_age,将175存入堆区,该堆区首地址(如0x0011)传给m_height

  • 之后调用默认构造函数(未定义拷贝构造函数的情况下)Person p2(p1),由于使用编译器所拷贝构造函数,会做浅拷贝操作,所以拷贝效果为:

    p2.m_age = p1.m_age = 20
    p2.m_height = p1.m_height = 0x0011 
    
  • 在主函数执行完test01()后,编译器将栈上的p1p2变量释放,会调用Person类的析构函数~Person(){}

  • 由先进后出原则可知先释放p2变量,执行析构函数:p2满足p2.m_height != 0,故释放0x0011指向的堆区,并将该指针指向空

  • 再释放p1变量,执行析构函数:p1满足p1.m_height != 0,故释放0x0011指向的地址。但是在上一步已经将0x0011地址指向的堆区释放过了,这就出现了重复释放堆区内存的非法操作

所以需要进行深拷贝操作,如代码中的拷贝构造函数Person(const Person& p){}所示

  • 对非指针变量进行普通的拷贝赋值:m_age = p.m_age

  • 对指针变量进行深拷贝操作:m_height = new int(*p.m_height);,这样就可以在拷贝时额外开辟一片堆区内存空间用于存p.m_height解引用的值,并将该片堆区首地址传回给m_height,避免了p1.m_height的值直接传给p2.m_height

    深拷贝

什么时候需要提供析构函数:若堆区开辟内存空间,则需要自己提供析构函数进行堆区空间释放

什么时候需要提供拷贝构造函数?:若属性有在堆区开辟的,一定要自己提供拷贝构造函数,进行深拷贝,防止浅拷贝带来的问题

使用浅拷贝与深拷贝的效果分别如下:

11.2.6 初始化列表

作用:一种初始化属性列表的方法,只是一种简化代码的初始化方法,

语法:构造函数():属性1(值1),属性2(值2),……{}

举例:

class Person {
public:
	Person(int a, int b, int c) :m_a(a), m_b(b), m_c(c) {
	}

    /*等同于常见初始化方法,只是单纯减少了代码行数:
    Person(int a, int b, int c){
    m_a = a;
    m_b = b;
    m_c = c;
    }*/
    
	int m_a;
	int m_b;
	int m_c;
};

void test() {
	Person p(10, 20, 30);
}
11.2.7 类对象作为类成员

解释:一个类的实例化对象也可作为另一个类的成员变量

举例:

class Phone {
public:
	Phone(string phone) :m_phone(phone) {
	}
	string m_phone;
};

class Person {
public:
	// Phone M_phone = phone; // 隐式转换法
	Person(string name,string phone):M_name(name),M_phone(phone){	//这行的 M_phone(phone) 相当于做了上述隐式转换法的构造函数调用
	}
	string M_name;
	Phone M_phone; 
};

void test() {
	Person p1("张三", "HUAWEI");
	cout << p1.M_name << "有" << p1.M_phone.m_phone << endl;	
    //注意:这里的p1.m_phone.m_phone,p1是Person类的对象,M_phone是Phone类的对象,m_phone是Phone类的属性
}

构造与析构顺序:构造时先小后大,析构时先大后小(本例中Person类为大)

11.2.8 静态成员

定义:静态成员是在成员变量或成员函数前加关键字static。静态成员包括 静态成员变量静态成员函数

静态成员变量

  • 类内声明,类外初始化

    class Person{
    public:
        static string m_name;	// 类内声明
    };
    
    string Person::m_name = "David";	// 类外初始化
    

    注意:静态成员变量的类外初始化时格式要严格按照:变量类型 类名::变量名 = 变量值

  • 所有对象共享同一份数据

    • 静态成员变量不单独属于任一实例化对象,因此对静态变量的访问方式有两种

      • 通过对象访问

        Person p1;
        cout << p1.m_name << endl;
        
      • 通过类名访问

        cout << Person::m_name << endl;
        
    • 同一类的所有对象的静态成员变量指向同一片内存空间

      string Person::m_name = "张三"; 
      Person p1,p2;					// 实例化两个对象
      cout << p1.m_name << endl;		// 输出:张三
      p1.m_name = "李四";			   
      cout << p2.m_name << endl;		// 输出:李四
      Person::m_name = "王五";
      cout << p1.m_name << endl;		// 输出:王五
      
  • 在编译阶段分配内存

静态成员函数

  • 所有对象共享同一个函数

    • 同上,静态成员函数同样有两种调用方式

      • 通过对象访问

        static void func(){}
        p1.func();
        
      • 通过类名访问

        Person::func();
        
  • 静态成员函数只能访问静态成员变量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值