一、对象的构造
1.前言
创建一个对象时,常常需要某些初始化的工作,比如数据成员赋初值。注意,类的成员数据是不能再声明类时初始化的。为了解决这个问题,C++编译器提供了构造函数(constructor)来处理的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是创建对象时自动执行。
2.构造函数
1)构造函数的定义:
·C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数
·构造函数在定义时可以有参数,也可以没有参数
·没有任何返回类型的声明
2)无参构造函数
#include <iostream>
using namespace std;
class Animal
{
public:
//无参构造函数
Animal()
{
//对成员变量进行初始化
age = 5;
cout <<"Animal"<<endl;
};
int get_age()
{
return age;
};
private:
int age;
};
int main() {
Animal a;//会自动调用一次构造函数 输出 Animal
cout<<a.get_age()<<endl;
return 0;
}
3)有参构造函数
#include <iostream>
using namespace std;
class Animal
{
public:
//无参构造函数
Animal()
{
//对成员变量进行初始化
_age = 5;
cout <<"Animal()"<<endl;
};
//有参构造函数(形参数量看情况)
Animal(int age,int weight)
{
cout<< "Animal(int age,int weight)"<<endl;
_age = age;
_weight = weight;
}
Animal(int age) {
cout << "Animal(int age)" << endl;
_age = age;
}
int get_age()
{
return _age;
}
private:
int _age;
int _weight;
};
int main() {
Animal a;//会自动调用一次构造函数 输出 Animal
Animal a1(10,20); //调用Animal(int age,int weight)
cout<<a1.get_age()<<endl; //10
Animal a2(20); //调用Animal(int age)
cout<<a2.get_age()<<endl; //20
return 0;
}
3.构造函数注意事项
如果在类中实现了带参数的构造函数,一定要实现一个无参的构造函数,因为如果在构造对象时不带参数将无法找到无参的构造函数导致编译失败。
4.初始化成员列表的使用
·由逗号分隔的初始化列表组成(前面带冒号)
·位于参数列表的右括号之后、函数体左括号之前
·如果数据成员的名称为mdata,并需要将它初始化为val,则初始化器为mdata(val)
class Animal
{
//访问控制符
public:
/* 初始化成员列表 */
Animal(int weight,int age):_weight(weight),_age(age)
{
cout<< "Animal(int age,int weight)"<<endl;
}
int get_age()
{
return _age;
}
int get_weight()
{
return _weight;
}
private:
//属性
int _weight;
int _age;
};
int main() {
Animal A(10,20); //使用初始化成员列表
cout<<A.get_age()<<endl;
cout<<A.get_weight()<<endl;
return 0;
}
问题:初始化成员列表什么时候必须使用呢?
1) 成员变量是引用的时候
public:
Animal(int weight,int age):_weight(weight),_age(age),p(_height)//引用初始化
{
cout<< "Animal(int age,int weight)"<<endl;
}
int get_age()
{
return _age;
}
int get_weight()
{
return _weight;
}
private:
//属性
int _weight;
int _age;
int _height;
int& p; //引用
};
2)成员变量被const修饰的时候
public:
Animal(int weight,int age):_weight(weight),_age(age),p(100)//初始化const修饰的变量
{
cout<< "Animal(int age,int weight)"<<endl;
}
int get_age()
{
return _age;
}
int get_weight()
{
return _weight;
}
private:
//属性
int _weight;
int _age;
int _height;
const int p;//const修饰变量
};
3)成员变量是另一个类的实例化对象,且对应的类中没有实现无参构造函数的时候
class B
{
public:
int age ;
B(int a)
{
age = a;
}
};
class Animal
{
//访问控制符
public:
/* a(10)在构造a对象的时候传参,会调用B(int)有参数的构造函数,从而绕过了调用B()无参构造函数 */
Animal(int weight,int age):_weight(weight),_age(age),a(10)
{
cout<< "Animal(int age,int weight)"<<endl;
}
int get_age()
{
return _age;
}
int get_weight()
{
return _weight;
}
private:
//属性
int _weight;
int _age;
int _height;
B a;//创建一个B类成员变量
};
5.总结
1)构造一个对象一定会自动调用一个构造函数
2)如果一个类没有实现默认构造函数,编译器会自动生成一个,前提是没有实现带参数的构造函数
3)如果一个类中实现了带参数的构造函数,一定要实现一个无参的构造函数,因为如果在构造对象时不带参数将无法找到无参的构造函数导致编译失败
4)构造函数可以有多个,根据构造对象时所传递的参数,会自动调用对应的构造函数
5)类不会占用程序的内存空间,对象才会占用程序的内存空间
二、对象的析构
1、析构函数定义和使用
·C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
语法:~ClassName()
·析构函数没有参数也没有任何返回类型的声明
class Animal
{
public:
int age;
char name[4];
/* 默认构造函数 */
Animal()
{
cout<< "Animal()"<<endl;
}
/* 析构函数 */
~Animal()
{
cout<<"~Animal()"<<endl; //程序结束自动调用
}
};
·构造函数在对象销毁时自动被调用
·析构函数调用机制:C++编译器自动调用
class Animal
{
public:
int age;
char *name;
/* 默认构造函数 */
Animal()
{
name = (char*)malloc(4);//在堆上为指针name申请空间
cout<<"Animal()"<<endl;
}
Animal(const char* name1)
{
cout<< "Animal(const char* name1)"<<endl;
int len;
len = strlen(name1);
//在堆上为指针name申请空间
name = (char*)malloc(len+1);//+1因为要包含‘\0’
strcmp(name,name1);
}
/* 析构函数 */
~Animal()
{
cout<<"~Animal()"<<endl;
if(name!=NULL)
free(name);//释放掉在堆上申请的空间
}
};
三、多个对象构造和析构
1、当类中的成员变量为另外一个类的实例化对象时,我们称这个对象为成员对象
2、成员变量所属的类中没有实现无参构造函数的时候,需要使用初始化成员列表
class bird
{
public:
bird()
{
cout<<"bird()"<<endl;
}
~bird()
{
cout<<"~bird()"<<endl;
}
};
class Animal
{
public:
Animal(int age,int len)
{
cout<<"Animal(int age,int len)"<<endl;
}
~Animal()
{
cout << "~Animal()" << endl;
}
private:
int _age;
int _len;
bird a;
bird b;
};
int main()
{
Animal a(10,10);
return 0;
}
3、构造函数和析构函数的调用顺序:当类中有成员变量是其他类的对象时,首先调用成员对象的构造函数,调用顺序与声明顺序相同;之后调用自身的构造函数;析构函数的调用顺序与对应的构造函数调用顺序相反。
四、使用已构造的对象初始化新的对象
1、直接使用已初始化的t1 初始化t2 :Test t2 =t1;
using namespace std;
class Test
{
public:
int *sum;
int x;
int y;
Test()
{
cout<<"Test()"<<endl;
x=0;
y=0;
sum=new int[4];
}
Test(int a,int b):x(a),y(b)
{
cout<<"Test(int a,int b):x(a),y(b)"<<endl;
sum = new int[4];
}
~Test()
{
cout<<"~Test()"<<endl;
delete[] sum;
}
};
int main()
{
Test t1(10,20);//调用构造函数:Test(int a,int b):x(a),y(b)
t1.sum[0]=100;
t1.sum[1]=101;
t1.sum[2]=102;
t1.sum[3]=103;
//使用已经初始化好的对象t1初始化一个新的对象
Test t2 =t1;
cout<<t2.x<<endl;
cout<<t2.y<<endl;
cout<<t1.sum<<endl;
cout<<t2.sum<<endl;
return 0;
}
2、使用指针变量初始化
Test *t1 = new Test(10,20);Test t2= *t1;
class Test
{
public:
int *sum;
int x;
int y;
Test()
{
cout<<"Test()"<<endl;
x=0;
y=0;
sum=new int[4];
}
Test(int a,int b):x(a),y(b)
{
cout<<"Test(int a,int b):x(a),y(b)"<<endl;
sum = new int[4];
}
~Test()
{
cout<<"~Test()"<<endl;
delete[] sum;
}
};
int main()
{
Test *t1 = new Test(10,20);//调用构造函数:Test(int a,int b):x(a),y(b)
t1->sum[0]=100;
t1->sum[1]=101;
t1->sum[2]=102;
t1->sum[3]=103;
Test t2= *t1;
cout<<t2.x<<endl;
cout<<t2.y<<endl;
cout<<t1->sum<<endl;
cout<<t2.sum<<endl;
/*手动释放t1申请的堆空间,意味着销毁掉*t1这个对象,
会自动调用析构函数,顺带手动释放成员变量sum所申请的空间*/
delete t1;
cout<<t2.sum[0]<<endl;
cout<<t2.sum[1]<<endl;
cout<<t2.sum[2]<<endl;
cout<<t2.sum[3]<<endl;
//t2调用了析构函数重复释放了sum导致程序异常终止
return 0;
}
重复释放sum 导致程序异常终止
3、解决同一块堆空间释放两次的情况,定义一个对象t2,然后使用赋值的方法
class Test
{
public:
int *sum;
int x;
int y;
Test()
{
cout<<"Test()"<<endl;
x=0;
y=0;
sum=new int[4];
}
Test(int a,int b):x(a),y(b)
{
cout<<"Test(int a,int b):x(a),y(b)"<<endl;
sum = new int[4];
}
~Test()
{
cout<<"~Test()"<<endl;
delete[] sum;
}
};
int main()
{
Test t1(10,20);//调用构造函数:Test(int a,int b):x(a),y(b)
t1.sum[0]=100;
t1.sum[1]=101;
t1.sum[2]=102;
t1.sum[3]=103;
//使用已经初始化好的对象t1初始化一个新的对象
Test t2 ; //定义一个t2,使用赋值的方式初始化
t2.x = t1.x;
t2.y = t1.y;
memcpy(t2.sum,t1.sum,4*sizeof(int));//使用拷贝函数
cout<<t1.sum<<endl;
cout<<t2.sum<<endl;
for(int i=0;i<4;i++)
cout<<t2.sum[i]<<endl;
return 0;
}
由于指向sum的指针不一样,因此不存在同一空间被释放两次。
五、拷贝构造函数的使用
1、系统自动调用的拷贝构造函数:Test(const Test &t) // t引用的是 右值
class Test
{
public:
int *sum;
int x;
int y;
Test()
{
cout<<"Test()"<<endl;
x=0;
y=0;
sum=new int[4];
}
Test(int a,int b):x(a),y(b)
{
cout<<"Test(int a,int b):x(a),y(b)"<<endl;
sum = new int[4];
}
//拷贝构造函数
Test(const Test &t) // t引用的是 右值
{
cout<<"Test(const Test &t)"<<endl;
x=t.x;
y=t.y;
sum = new int[4];
//将t.sum所指向的空间中的内容拷贝到sum所指向的空间中
for(int i =0;i<4;i++)
sum[i]=t.sum[i];
}
~Test()
{
cout<<"~Test()"<<endl;
delete[] sum;
}
};
int main()
{
Test t1(10,20);//调用构造函数:Test(int a,int b):x(a),y(b)
t1.sum[0]=100;
t1.sum[1]=101;
t1.sum[2]=102;
t1.sum[3]=103;
//使用已经构造好的对象t1初始化一个新的对象
Test t2=t1;//会自动调用拷贝构造函数 t2.Test(t1);
cout<<t1.sum<<endl;
cout<<t2.sum<<endl;
for(int i =0;i<4;i++)
cout<<t2.sum[i]<<endl;
return 0;
}
2、拷贝构造函数的形参解释
Test(const Test &t)
使用const修饰形参,是为了只允许拷贝对象,不允许修改对象,保护右值;
使用&引用是因为:如果没有使用&修饰对象,调用拷贝构造函数Test(const Test t)时,t2.Test(t1)又相当于Test t = t1(等于又创建了一个新的对象,降低程序运行效率),并且又将出现同一个指向sum的指针。
六、深拷贝和浅拷贝
1、浅拷贝
1)概念:同一类型的对象之间可以赋值,使得两个对象的成员变量值相同,两个对象仍然是独立的两个对象。
Test t2=t1;//会自动调用拷贝构造函数
//拷贝构造函数
Test(const Test &t) // t引用的是 右值
{
cout<<"Test(const Test &t)"<<endl;
x=t.x;
y=t.y;
sum = t.sum;
}
2)一般情况下,浅拷贝没有任何副作用,但是当类中又指针,并且指针指向动态分配的内存空间,将导致两个对象的指针变量指向同一块内存空间,当两个对象被销毁时调用析构函数,因为析构函数中会释放指针所指向的堆空间,造成同一块堆空间被释放两次从而导致程序运行出错。
~Test()
{
cout<<"~Test()"<<endl;
delete[] sum;
}
3)如果我们没有实现拷贝构造函数,C++编译器会自动实现一个拷贝函数,称之为默认拷贝函数,但是在默认拷贝函数中实现的是浅拷贝。
2、深拷贝
实现拷贝构造函数,在拷贝构造函数中需要对对象中的指针变量进行单独的内存申请。两个对象中的指针变量不会指向同一块内存空间,然后在将右值对象指针所指向的空间中的内容拷贝到新的对象所指向的堆空间中。
Test(const Test &t) // t引用的是 右值
{
cout<<"Test(const Test &t)"<<endl;
x=t.x;
y=t.y;
sum = new int[4];
//将t.sum所指向的空间中的内容拷贝到sum所指向的空间中
for(int i =0;i<4;i++)
sum[i]=t.sum[i];
}