文章目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类 中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。他们是特殊的成员函数,特殊的点非常多,如果我们不实现,编译器会自己生成一份默认的,如果实现,编译器则不会生成一份默认的。
构造函数
为什么要有构造函数?我们要写一个公有的成员函数让成员变量进行初始化。每次实例化一个对象,都要调用该成员函数,是不是很麻烦呢?那能否在对象创建时,就将信息设置进去呢?
构造函数概念
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数就是在实例化对象时自动调用构造函数进行初始化。
例如,以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。构造函数不只是只有一个,可以根据初始化的需求进行重载。
构造函数的特性
1.函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?不能
int main()
{
Date d1;
return 0;
}
6 . 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
看看以下代码:
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化
d1.Print();
return 0;
}
运行结果:
编译器生成的默认构造函数,对成员变量进行了随机值初始化。
关于编译器生成的默认成员函数,会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如
int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现
编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
小结:
编译器自动生成的构造函数机制:
1、编译器自动生成的构造函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认构造函数。
这里涉及初始化列表,先铺垫一下,初始化列表默认调用自定义类型的无参构造函数,初始化列表作用在于初始化而不是赋值,构造函数体才是赋值操作,上述代码默认的初始化列表充当 Time _t();int _year;
int _month;int _day;初始化的过程。
析构函数
析构函数的概念
析构函数:与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
我们知道当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,例如,我们用日期类创建了一个对象d1,当d1被销毁时,对象d1当中的局部变量_year/_month/_day也会被编译器销毁。
但是这并不意味着析构函数没有什么意义。像栈(链表)这样的类对象,当该对象被销毁时,其中动态开辟的节点并不会随之被销毁,需要我们对其进行空间释放,这时析构函数的意义就体现了。
析构函数的特性
1 . 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
编译器自动生成的析构函数机制:
1、编译器自动生成的析构函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。
#include <iostream>
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time()\n";
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
4 、对象生命周期结束时,C++编译系统系统自动调用析构函数。
5、先构造的后析构,后构造的先析构
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。
小结: 无论有没有写析构函数,如果有自定义内型成员,那么一定会调用该自定义成员的析构函数,原因也很简单,每个类只有一个析构函数,编译器为了安全必须自动调用。即析构函数只处理非自定义内型成员。如果析构函数不释放动态开辟的成员,会导致内存泄漏。
拷贝构造函数
拷贝构造函数的概念
拷贝构造函数也是构造函数,但它只有一个参数,这个参数是本类 的对象(不能是其他类的对象),而且采用对象的引用的形式只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数的特征
1、拷贝构造函数是构造函数的一个重载形式。
2、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)// 拷贝构造函数
{
_year = d._year;// const 的修饰防止, d._year=_year;写错后,把“右值”给修改了
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 31);
Date d2(d1); // 用已存在的对象d1创建对象d2
return 0;
}
拷贝构造函数传参为什么必须传引用呢?
上图分析:
简单点就是
调用拷贝构造1,需要先传参,传值传参又是一个拷贝构造2,
调用拷贝构造2,需要先传参,传值传参又是一个拷贝构造3,
调用拷贝构造3,需要先传参,传值传参又是一个拷贝构造4,
……不断的调用Date(const Date date)函数;
3 、若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 30);
Date d2(d1); // 用已存在的对象d1创建对象d2
d1.Print();
d2.Print();
return 0;
}
代码中,我们自己并没有定义拷贝构造函数,但编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造。
编译器自动生成的拷贝构造函数机制:
1、编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。
2、对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数。
4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
class String
{
public:
String(char * p)
{
_str = p;
}
//浅拷贝
/*String(const String& Str)
{
_str = Str._str;
}*/
// 深拷贝
String(const String& Str)
{
char* _str =(char*)malloc(sizeof(Str._str));
strcpy(_str, Str._str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
const char* str = "hello";
char* p = (char*)malloc(sizeof(str));
for (int i = 0; i < sizeof(str); i++)
p[i] = str[i];
String s1(p);
String s2(s1);// 浅拷贝报错
}
浅拷贝与深拷贝的对比图:
总结一下:
1、像Date这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,我们不需要自己写。
2、像String这样的类,浅拷贝会导致析构两次、程序崩溃等问题,需要我们自己写对应的拷贝构造函数。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型或枚举类型的操作数。
3.用于内置类型的操作符,重载后其含义不能改变。
4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。
这里以重载 == 运算符作为例子:
我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
bool operator==(const Date& d)// 运算符重载函数
{
return _year == d._year
&&_month == d._month
&&_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,7,10);
Date d2(2022,7,10);
cout<<(d1==d2)<<endl;
cout<<(d1.operator==(d2))<<endl;//一样
}
也可以将该运算符重载函数放在全局域,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)不过这样破坏了封装。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
return d1._year == d2._year
&&d1._month == d2._month
&&d1._day == d2._day;
}
int main()
{
Date d1(2022,7,10);
Date d2(2022,7,10);
cout<<(d1==d2)<<endl;
cout<<(operator==(d1,d2))<<endl;//一样
}
赋值运算符重载
这里以重载 = 运算符作为例子:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)// 赋值运算符重载函数
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()// 打印函数
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
赋值运算符主要有四点:
1、 参数类型
2、 返回值
3、 检测是否自己给自己赋值
4、返回*this
5、 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
6、深拷贝和浅拷贝的问题
默认生成的赋值运算符函数
编译器默认生成赋值重载,根拷贝构造函数完全类似
1.内置类型成员,会完成字节序拷贝–浅拷贝
2.自定义类型成员变量,会调用它的operator=
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2018,10, 1);
// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
d1 = d2;
return 0;
}
const成员
const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如图:
我们来看看下面代码:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void ShowDate()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void ShowDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// ShowDate()函数与ShowDate()const函数重载
const Date d1;
d1.ShowDate();//const 对象调用const修饰的成员函数
Date d2;
d2.ShowDate();//d2调用非const修饰的成员函数
return 0;
}
回答以及下金典问题:
1.const对象可以调用非const成员函数吗?
2.非const对象可以调用const成员函数吗?
3.const成员函数内可以调用其他的非const成员函数吗?
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗?
答案是:不可以、可以、不可以、可以
解释如下:
1.非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。
2.const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。
3.在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。
4.在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。
取地址及const取地址操作符重载
取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了。
class Date
{
public:
Date* operator&()// 取地址操作符重载
{
return this;
}
const Date* operator&()const// const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
对6个默认成员函数总结
在写默认成员函数时,
构造函数默认情况都会调用自定义类型的默认成员函数,写构造时,只需要考虑非自定义类型即可。除了一些自定义类型需要在初始化列表指定初始化参数进行调用自定义类型的构造函数初始化外。
析构函数,考虑非自定义内型内存泄漏问题,没有资源需要清理,不用自己实现析构函数,反之……。自定义内型的会默认调用自己的析构,每个类的析构函数只有一个。在发生内存泄漏时检查每个类的析构函数。
拷贝构造,注意深拷贝和浅拷贝的问题,对内置类型默认是字节序拷贝,参数必须是引用对象,自定义类型调用自定义类型的拷贝构造。
赋值运算符,注意深拷贝和浅拷贝的问题,默认是浅拷贝,参数是引用对象,返回值引用对象,
学习了4个默认生成成员函数总结:
1、构造和析构处理机制是基本类似的。(默认生成的对内置不处理,处理自定义调用它的函数)
2、拷贝构造和赋值运算符重载处理机制基本类似的。(默认生成的对内置和自定义都处理,浅拷贝);
【面试题】
如下代码一共调用了几次构造函数。一般情况是三次,但是编译器优化了一次。一次调用里面,连续构造函数,会被编译器优化,合二为一;
Widget f(Widget u()
{
return u;
}
int main()
{
Widget x;
Widget y = f(x);
return ;
}
解析:
f(x)传参时调用拷贝构造,返回值时调用拷贝构造生成临时对象并且接下来临时对象要初始化y对象要调用y对象的拷贝构造,连续两次调用,编译器优化掉给返回对象的拷贝构造,不用临时对象给y对象初始化,直接使用u对象进行拷贝构造y对象。