目录
前言:
一、类的默认成员函数:
二、构造函数:
1.特性:
构造函数调用规则:
1.无参数的构造函数(默认构造函数):
2.带参数的构造函数:
3.全缺省的构造函数(默认构造函数):
三、析构函数:
特性:
四、拷贝构造函数:
总结:
前言:
上一篇文章我们浅浅的了解类与对象,知道了如何定义一个类,而今天我们更加深入的了解类与对象,也就是了解类的六个默认成员函数。接下来让我们进入学习。
一、类的默认成员函数:
如果一个类中什么都没有,称之为空类。虽然称之为空类,但它并不是真正意义上的空类,虽然我们没有给它写任何东西,但是编译器会自动的生成六个默认成员函数。
默认成员函数:用户没有显示实现时,编译器自动生成的成员函数叫做默认成员函数。
默认成员函数有6个可以分为三类:
①.初始化和清理:
构造函数主要完成初始化工作;
析构函数主要完成清理工作。
②.拷贝复制:
拷贝构造是使用同类对象初始化创建另一个对象;
赋值重载是把一个对象赋值给另一个对象。
③.取地址重载:
主要是普通对象和const对象取地址。
本篇博客我们重点介绍前面三个默认成员函数:构造函数、析构函数、拷贝构造函数。
二、构造函数:
大家回想一下之前写一个Stack的时候,是不是有一个Init函数用来初始化,但是我们在使用的时候又可能忘记使用这个函数导致忘记初始化,编译器就会报错。构造函数就是为了避免这种错误发生而生的。
1.特性:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开 空间创建对象,而是初始化对象。
构造函数特征如下:
①.名字与类名相同;
②.没有返回值;
③.对象实例化时编译器自动调用;
④.可以构成重载;
下面我们来看看下面这段代码:
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
Date(int year, int month, int day)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022, 5, 22);
return 0;
}
代码运行结果:
构造函数调用规则:
1.无参数的构造函数(默认构造函数):
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
在这函数中,构造函数并没有传入参数,所以叫做无参数的构造函数。只要示例化就会调用这个函数。
2.带参数的构造函数:
class Date
{
public:
Date(int year, int month, int day)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d2(2022, 5, 22);
return 0;
}
在这个函数中,构造函数传入了参数,所以叫带参数的构造函数,只需向调用函数一样就可以。
3.全缺省的构造函数(默认构造函数):
class Date
{
public:
Date(int year=1, int month=2, int day=20)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022, 5, 22);
return 0;
}
这和带参的构造函数差不多,也是一样的调用,有参数时候按照方式2来传参数,没有参数的时候按照方式1来调用。我们再来看看这段代码:
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
Date(int year, int month, int day)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
Date(int year=1, int month=2, int day=20)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022, 5, 22);
return 0;
}
为什么代码运行的时候为报错呢?
虽然构造函数之间在语法上构成重载函数,但是我们在调用的时候,会存在调用不明确的问题,也就是说不知道该调用那个构造函数。
当用户没有自己写构造函数的时候,编译器会自动生成一个构造函数,称之为默认构造函数,默认构造函数只能有一个。
ps:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
我们来看看编译器生成的默认构造函数:
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d2();
return 0;
}
我们可以看到编译器生成的默认构造函数,但是看起来默认构造函数又没什么用?d2对象调用了编译器生成的默认构造函数,但是d对象 _year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如: int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。也就是说默认构造函数,对内置类型成员不做处理,自定义类型成员会去调用它的默认构造(不用传参数的构造);
三、析构函数:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成鹅,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性:
析构函数特性如下:
①.析构函数实在类名前加~;
②.无参数无返回值类型;
③.一个类只能有一个析构函数,若用户未显示定义,系统会自动生成默认的析构函数。析构函数不支持重载;
④.对象生命周期结束时,c++编译系统自动调用析构函数。
class Stack
{
public:
Stack(int capcity = 4, int top = 0)
{
_capcity = capcity;
_top = top;
}
~Stack()
{
cout << "~Stack()" << endl;
}
private:
int _capcity;
int _top;
int* _a;
};
int main()
{
Stack d;
return 0;
}
可以看到我们并没有去调用析构函数,编译器自动帮我们调用了析构函数。默认生成的析构函数与默认生成的构造函数一样,对内置类型成员不做处理。自定义类型成员会去调用它的默认析构函数。析构函数的调用遵循后进先出原则,也就说后定义的对象先进行析构函数的调用。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
}
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
private:
int _year;
int _month ;
int _day ;
Time t1;
};
int main()
{
Date d;
return 0;
}
在main中没有创建Time类的对象,但还是打印了Time和~Time,可见析构函数确实会对自定义类型调用其自己的析构函数。(构造函数同理)
四、拷贝构造函数:
只有单个形参,该形参时本类类对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
1.拷贝构造函数是构造函数的一个重载形式,一下是拷贝构造函数示例:
class Date
{
public:
Date(int year = 1, int month = 2, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month ;
int _day ;
};
int main()
{
Date d1(2023,5,23);
Date d2 = d1;
d1.Print();
d2.Print();
return 0;
}
可以看到d2确实和d1的值是一样的,说明拷贝成功了。
拷贝构造函数参数中的const有什么用呢?
解答:这里加const是为了防止将谁是谁的拷贝的位置写反了,一旦写错不加const那么用来拷贝的那个类里面的东西都被修改了。这里加const还有第二个好处,就是防止权限放大。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
为什么会引发无穷递归调用呢?
因为我们在传值传参时候,只要是自定义类型那么编译器会自动调用拷贝构造函数,举个例子:Date(Date d)如果这是拷贝构造函数,在调用这个函数的时候发现参数为自定义的Date类型那么就会调用拷贝构造函数,此时又出现了Date(Date d),所以会一直无穷的递归下去。
为什么使用引用传值就不会无穷递归?
因为引用就是本身,在调用也是本身不会去再调用拷贝构造函数。
3.当用户没有写拷贝构造函数时,编译器会自动生成拷贝构造函数。
class Date
{
public:
Date(int year = 1, int month = 2, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month ;
int _day ;
};
int main()
{
Date d1(2023,5,23);
Date d2 (d1);
d1.Print();
d2.Print();
return 0;
}
通过上面的代码我们可以发现及时我们不写拷贝构造函数编译器也能完成日期类的拷贝,那要是其他类型呢?我们来看看下面这段代码:
class Stack
{
public:
Stack(int top = 0,int capcity=4)
{
_a = (int*)malloc(sizeof(int*) * capcity);
_top = top;
_capcity = capcity;
}
private:
int* _a;
int _top;
int _capcity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
从结果来看我们好像拷贝成功了,实际上并没有,s1和s2的_a其实是指向同一块地址的。当我们结束s1和s2对象时,_a会被清理两次。而且当我们的修改s1_a的内容时,s2的也会跟着修改。
为什么拷贝构造会造成上述这个情况?
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
什么情况下会调用拷贝构造呢?
1.显示调用
2.传值传参
3.自定义类型做返回值
总结:
初学者对于构造函数,析构函数,拷贝构造函数时一大难点,所以需要下去多练习多思考。