对象的初始化和清理是两个非常重要的安全问题,创建一个对象而没有初始化,使用的后果将是未知的,使用完一个对象而没有及时清理,也会造成一定的安全问题。C++利用构造函数和析构函数解决上述问题,它们将会被编译器自动调用,完成对象的初始化和清理。
一、构造函数和析构函数的语法规则
对象的初始化和清理是强制性的,如果用户不定义构造函数和析构函数,编译器会自动定义默认的构造函数和析构函数。
1.构造函数
函数名与类名相同;没有返回值,也不用写void;可以有参数,因此能发生重载;创建对象时会自动调用,无须手动调用,且仅调用一次。
2.析构函数
函数名与类名相同,但要在函数名前加上“~”;没有返回值,也不用写void;不可以有参数,因此不能发生重载;销毁对象前会自动调用,无须手动调用,且仅调用一次。
class Person
{
public:
//构造函数
//函数名与类名相同;没有返回值,也不用写void
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
//函数名与类名相同,但要在函数名前加上“~”;没有返回值,也不用写void
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
void test()
{
//创建对象时会自动调用构造函数,无须手动调用,且仅调用一次。
Person p;
//销毁对象前会自动调用析构函数,无须手动调用,且仅调用一次
}
int main()
{
test();
return 0;
}

二、常用构造函数及构造函数的分类
由于构造函数可以有参数,所以根据有无参数可分为无参构造和有参构造。根据类型不同则可以分为普通构造和拷贝构造。一般在代码编写过程中会用到三种主要的构造函数。
1.默认构造函数
具体分类为无参构造函数——普通构造函数,该构造函数一般由编译器自动定义,内容为空,主要作用是确保对象会被初始化。对于包含默认构造函数的类,在创建对象时只有一种调用方式。
2.有参构造函数
具体分类为有参构造函数——普通构造函数,该构造函数的主要作用是在通过类创建对象时,按照程序员的需求将对象初始化。对于包含有参构造函数的类,在创建对象时有三种调用方式,分别为括号法、显示法和隐式转换法,一般使用括号法。
3.拷贝构造函数
具体分类为有参构造函数——拷贝构造函数,该构造函数的主要作用是令对象具有可拷贝的性质,即使得“类和对象”在使用时可以视为“数据类型和变量”。对于包含拷贝构造函数的类,在创建对象时也有三种调用方式,分别为括号法、显示法和隐式转换法,一般使用括号法。
class Person
{
public:
string Nationality;
string Car;
string Phone;
//构造参数,可以有参数,所以能发生重载
//无参构造函数——普通构造函数,一般称为默认构造函数
Person()
{
cout << "无参构造函数——普通构造函数" << endl;
}
//有参构造函数——普通构造函数,一般称为有参构造函数
Person(string nationality,string car,string phone)
{
Nationality = nationality;
Car = car;
Phone = phone;
cout << "有参构造函数——普通构造函数" << endl;
}
//有参构造函数——拷贝构造函数,一般称为拷贝构造函数
Person(const Person& p)
{
Nationality = p.Nationality;
Car = p.Car;
Phone = p.Phone;
cout << "有参构造函数——拷贝构造函数" << endl;
}
//析构参数,不可以有参数,所以不能发生重载
~Person()
{
cout << "析构函数" << endl;
}
};
void test()
{
//包含默认构造函数的类创建对象,此为唯一方法
Person p11;
//包含默认构造函数的类创建对象时不能加括号,否则编译器会认为这是一个函数声明
//Person p12();
//包含有参构造函数的类创建对象
//括号法
Person p21("中国","比亚迪","华为");
//显示法
//用此种方法创建对象时,系统会先创建一个匿名对象Person("中国","比亚迪","华为"),然后将其命名为p22
Person p22 = Person("中国", "比亚迪", "华为");
//隐式转换法
Person p23 = { "中国","比亚迪","华为" };
//下面这条语句也可单独执行,意为创建一个初始值为("中国", "比亚迪", "华为")的匿名对象
//不过在此句执行完毕后,就马上销毁该匿名对象
Person("中国", "比亚迪", "华为");
//包含拷贝构造函数的类创建对象
//括号法
Person p31(p21);
//显示法
Person p32 = Person(p22);
//隐式转换法
Person p33 = p23;
//不能使用拷贝构造函数创建匿名对象,编译器会认为Person(p33)===Person p33,即p33重定义
//Person(p33);
}
int main()
{
test();
return 0;
}

三、构造函数的定义规则
1.用户不定义任何构造函数
默认情况下,编译器会给一个类自动定义3个函数,分别为默认构造函数(无参,函数体为空)、默认拷贝构造函数(有参,对成员属性进行值拷贝,即浅拷贝)和默认析构函数(无参,函数体为空)。
class Person
{
public:
string Nationality;
string Car;
string Phone;
};
void test()
{
//用户未定义默认构造函数,但编译器会自定义,所以可以执行该语句
Person p1;
p1.Nationality = "中国";
p1.Car = "比亚迪";
p1.Phone = "华为";
cout << "p1.Nationality = " << p1.Nationality << endl;
cout << "p1.Car = " << p1.Car << endl;
cout << "p1.Phone = " << p1.Phone << endl << endl;
//用户未定义拷贝构造函数,但编译器会自定义,所以可以实现浅拷贝操作
Person p2(p1);
cout << "p2.Nationality = " << p2.Nationality << endl;
cout << "p2.Car = " << p2.Car << endl;
cout << "p2.Phone = " << p2.Phone << endl;
}
int main()
{
test();
return 0;
}

2.用户定义有参构造函数
编译器不再自动定义默认构造函数,但是会自动定义默认拷贝构造函数。
class Person
{
public:
string Nationality;
string Car;
string Phone;
//此时编译器不会定义默认构造函数,若用户未定义默认构造函数,则无法通过“Person p1;”方式创建对象
//默认构造函数
Person()
{
cout << "默认构造函数" << endl;
}
//有参构造函数
Person(string nationality, string car, string phone)
{
Nationality = nationality;
Car = car;
Phone = phone;
cout << "有参构造函数" << endl;
}
};
void test()
{
//此时执行该语句时使用的是用户自定义的默认构造函数
Person p1;
Person p2("中国", "比亚迪", "华为");
cout << "p2.Nationality = " << p2.Nationality << endl;
cout << "p2.Car = " << p2.Car << endl;
cout << "p2.Phone = " << p2.Phone << endl << endl;
//用户未定义拷贝构造函数,但编译器会自定义,所以可以实现浅拷贝操作
Person p3(p2);
cout << "p3.Nationality = " << p3.Nationality << endl;
cout << "p3.Car = " << p3.Car << endl;
cout << "p3.Phone = " << p3.Phone << endl;
}
int main()
{
test();
return 0;
}

3.用户定义拷贝构造函数
编译器不会再自动定义默认构造函数和默认拷贝构造函数。此时程序员必须要自己定义默认构造函数或有参构造函数,否则会无法创建对象。
class Person
{
public:
string Nationality;
string Car;
string Phone;
//此时编译器不会定义默认构造函数,因此默认构造函数和有参构造函数至少要定义一个,否则无法通过类创建对象
//默认构造函数
Person()
{
cout << "默认构造函数" << endl;
}
//有参构造函数
Person(string nationality, string car, string phone)
{
Nationality = nationality;
Car = car;
Phone = phone;
cout << "有参构造函数" << endl;
}
//拷贝构造函数
Person(const Person& p)
{
Nationality = p.Nationality;
Car = p.Car;
Phone = p.Phone;
cout << "拷贝构造函数" << endl;
}
};
void test()
{
//此时执行该语句时使用的是用户自定义的默认构造函数
Person p1;
Person p2("中国", "比亚迪", "华为");
cout << "p2.Nationality = " << p2.Nationality << endl;
cout << "p2.Car = " << p2.Car << endl;
cout << "p2.Phone = " << p2.Phone << endl << endl;
//此时拷贝时使用的是用户自定义的拷贝构造函数
Person p3(p2);
cout << "p3.Nationality = " << p3.Nationality << endl;
cout << "p3.Car = " << p3.Car << endl;
cout << "p3.Phone = " << p3.Phone << endl;
}
int main()
{
test();
return 0;
}

四、有参构造函数和拷贝构造函数的使用
1.有参构造函数的基本使用
在C语言中,在结构体定义时不能对成员变量初始化,当定义完成后,创建变量时能够依照用户需求初始化。
而在C++中,在结构体和类封装时对能够对成员属性初始化,当封装完成后,若用户未定义有参构造函数,创建对象时却反而不能依照用户需求初始化,这样就使得创建对象时的灵活性和便利性大大降低。
因此,为了使C++中类的功能更完善,在封装结构体和类时用户一般要自定义有参构造函数。而若要通过有参构造函数将对象初始化,一般采用初始化列表的方法。
class Person {
public:
////传统方式初始化
//Person(int a, int b, int c)
//{
// m_A = a;
// m_B = b;
// m_C = c;
//}
//初始化列表方式初始化的好处
//1、类成员中存在常变量和引用,而他们只能初始化而不能赋值
// 因此若采用传统方法无则法在创建对象时对他们初始化
//2、真正的初始化发生在构造函数执行之前,即初始化列表处
// 若采用传统方法,则需要经历初始化和赋值两步,增加了内存开销
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
{
}
void PrintPerson()
{
cout << "mA:" << m_A << endl;
cout << "mB:" << m_B << endl;
cout << "mC:" << m_C << endl;
}
private:
int m_A;
const int m_B;
int& m_C;
};
int main()
{
Person p(1, 2, 3);
p.PrintPerson();
return 0;
}

2.对象与对象成员
一个类A的成员可以是通过另一个类B创建的对象,此种成员称为对象成员。此时用户不仅要为类A定义有参构造函数,还必须要为类B定义有参构造函数,否则无法初始化对象a。
当程序执行时,会先通过类B创建对象b,然后再通过类A创建包含对象成员b的对象a,而当对象a所属函数执行完毕时,会先销毁对象a,再销毁对象b。
class Car
{
public:
string c_Nationality;
string c_Name;
//若要初始化对象Chou,用户必须要为类Car定义有参构造函数
Car(string c_nationality, string c_name) :c_Nationality(c_nationality), c_Name(c_name)
{
cout << "Car的构造函数" << endl;
}
~Car()
{
cout << "Car的析构函数" << endl;
}
};
class Person
{
public:
string p_Nationality;
//Car类创建的p_Car对象,是Person类的一个成员,称为对象成员
Car p_Car;
//若要初始化对象Chou,用户必须要为类Car定义有参构造函数
Person(string p_nationality, string p_c_nationality, string p_c_name) :p_Nationality(p_nationality), p_Car(p_c_nationality, p_c_name)
{
cout << "Person的构造函数" << endl;
}
~Person()
{
cout << "Person的析构函数" << endl;
}
void CheckIdentity()
{
cout << "Chou作为" << p_Nationality << "人,他开的当然是" << p_Car.c_Nationality << "车企的" << p_Car.c_Name << "电车" << endl;
}
};
void test()
{
//先创建对象成员p_Car,再创建对象Chou,可由构造函数的打印顺序证明
//先销毁对象Chou,再销毁对象成员p_Car,可由析构函数的打印顺序证明
Person Chou("中国", "中国", "比亚迪");
Chou.CheckIdentity();
}
int main()
{
test();
return 0;
}

3.拷贝构造函数的基本使用
在引入结构体和类之前,写代码实际上都可以看作是对数据类型和其所创建的变量进行操作。而引入之后,C++在结构体和类中定义了拷贝构造函数,这样在某种程度上就可以将“结构体和对象”及“类和对象”视为“数据类型和变量”。
一般有三种情况要使用拷贝构造函数,使用一个已创建的对象来初始化一个新对象;将一个已创建的对象作为函数参数进行值传递;将函数中的局部对象作为返回值进行值传递。
class Person
{
public:
int Age;
//用户定义了拷贝构造函数和有参构造函数,所以如果要用到默认构造函数,应由用户来定义
Person()
{
cout << "默认构造函数" << endl;
}
Person(int age)
{
cout << "有参构造函数" << endl;
Age = age;
}
//用户定义拷贝构造函数,用于观察何时使用了拷贝构造函数
Person(const Person& p)
{
cout << "拷贝构造函数" << endl;
Age = p.Age;
}
~Person()
{
cout << "析构函数" << endl;
}
};
//1、使用一个已创建的对象来初始化一个新对象
void test1()
{
//调用有参构造函数
Person man(15);
//调用拷贝构造函数
Person new_man1(man);
Person new_man2 = Person(man);
Person new_man3 = man;
//对新创建的对象赋值操作,不调用拷贝构造函数,但最终实现的功能与其相同
Person newman;
newman = man;
}
//2、将一个已创建的对象作为函数参数进行值传递
//函数dowork2()参数的值传递过程为Person newman = man;
void doWork2(Person newman)
{
cout << "函数dowork2()执行" << endl;
}
void test2()
{
//调用有参构造函数
Person man(30);
//调用拷贝构造函数
doWork2(man);
}
//3、将函数中的局部对象作为返回值进行值传递
Person doWork3()
{
//调用有参构造函数
Person man(40);
cout << (int*)&man << endl;
//调用拷贝构造函数
return man;
}
void test3()
{
//函数dowork3()返回值的值传递过程为Person newman = man;
Person newman = doWork3();
cout << (int*)&newman << endl;
}
int dowork4()
{
int a = 10;
cout << (int*)&a << endl;
return a;
}
void test4()
{
int new_a = dowork4();
cout << (int*)&new_a << endl;
}
int main()
{
test1();
cout << endl << endl << endl;
test2();
cout << endl << endl << endl;
test3();
cout << endl << endl << endl;
test4();
return 0;
}

对于第三种情况,从理论上来说,输出内容应为下图所示。程序执行到函数dowork3()中的语句“return man;”时会先执行“Person newman = man;”,此时调用拷贝构造函数,当“return man;”执行完毕后再调用析构函数销毁对象man,因此man和newman的地址不相同。

而实际上的输出内容却并非如此,这是因为C++为了减少消耗,使用了一项返回值优化技术。程序执行到函数test3()中的语句“Person newman = doWork3();”时编译器会先确定对象newman的地址,在函数doWork3()中当执行到作为返回值的局部对象man时,实际上并不创建新的对象man,而是仅创建一个对象名man,令其指向newman的地址,在函数doWork3()执行结束后,销毁的也仅仅是一个符号为man的对象名。也正因如此,man和newman的地址是相同的。
五、深拷贝与浅拷贝
在代码编写过程中,有时会需要在堆区开辟空间,将对象的成员属性存放于此。对堆区空间的控制由指令new开始,至指令delete结束。new指令在堆区空间M写入内容后返回其地址m,创建指针p接收地址m;delete指令将M释放,即将M中的内容清空且p也不再指向m。使用指令new和delete是使用堆区空间必须要有的两个环节。
在类的封装过程中,指针变量p创建为成员属性,new指令的使用位于实现对象初始化的有参构造函数,delete指令的使用位于实现对象销毁的析构函数中,这样即可保证对象、对象中存放于堆区的成员属性和对象中的其他成员具有相同生命周期。
如果用户不定义拷贝构造函数时,编译器会自动定义默认的拷贝构造函数,它的作用只是对成员属性进行简单的值拷贝,即浅拷贝。当创建对象a后,其成员属性中的指针a.p所存放的内容只是堆区空间地址m,若采用浅拷贝创建对象b,其成员属性中的指针b.p存放的内容依然是堆区空间地址m。在程序执行完毕后,需要销毁对象a和b,此时析构函数及其中delete指令会执行两次,但在代码运行过程中只开辟了一块堆区空间M,一块堆区空间重复释放两次将导致程序直接崩溃。原理如下图所示。

因此,需要由用户定义拷贝构造函数,实现深拷贝。
class Person
{
public:
int Age;
//创建指针变量接收堆区空间地址
int* Height;
//有参构造函数——使用new指令开辟堆区空间
Person(int age, int height)
{
cout << "有参构造函数" << endl;
Age = age;
Height = new int(height);
}
//拷贝构造函数——深拷贝
Person(const Person& p)
{
cout << "拷贝构造函数" << endl;
//在拷贝构造函数中再次使用new指令,这样每次拷贝时都会开辟新的堆区空间,避免堆区空间的重复释放
Age = p.Age;
Height = new int(*p.Height);
}
//析构函数——使用delete指令释放堆区空间
~Person()
{
cout << "析构函数" << endl;
if (Height != NULL)
{
delete Height;
}
}
};
void test()
{
Person Chou(22, 180);
cout << "Chou的年龄: " << Chou.Age << " 身高: " << *Chou.Height << endl;
Person Clone_Chou(Chou);
cout << "p2的年龄: " << Clone_Chou.Age << " 身高: " << *Clone_Chou.Height << endl;
}
int main()
{
test();
return 0;
}

本文围绕C++的构造函数和析构函数展开。介绍了它们的语法规则,常用构造函数的分类,包括默认、有参和拷贝构造函数,以及构造函数的定义规则。还阐述了有参和拷贝构造函数的使用场景,如对象初始化、对象成员处理等,最后强调了深拷贝和浅拷贝的区别及深拷贝的必要性。

849

被折叠的 条评论
为什么被折叠?



