一、类的6个默认成员函数
一个类中什么成员没有,叫空类。前面说了,空类实例化以后,系统会分配一个字节给它。
空类中什么都没有吗?不是,任何类,在什么都不写的时候,编译器还是会生成六个默认的成员函数。(贴心)
默认成员函数:用户没有显示实现,编译器也会自动生成的成员函数。
这六个成员函数就是天选之子。一开始就享受其他函数享受不了的待遇。
分别是: 构造函数和析构函数 、拷贝构造函数和赋值运算符重载 、 取地址操作符重载和const取地址操作符重载
二、构造函数
构造函数是默认的成员函数之一。
构造函数是一个特殊的成员函数,它的名字和类名相同,创建类的类型对象时由编译器自动调用,来保证每个数据成员都有初始值,并且在对象整个生命周期内只调用一次。
特点:
它的主要任务是初始化对象。对内置类型(int、char等语言提供的数据类型)初始化随机值,对自定义类型成员调用它的默认成员函数。
C++11中对于对内置类型的初始化随机值又打了补丁,就是可以在类中对内置类型的声明时,可以给默认值。
class Time
{
public:
Time()//time的构造函数,无返回值,函数名和类名相同
{
cout<< "time()"<<endl;//加这个是为了体现调用过这个time的构造函数。
_hour=12;
_minute = 12;
_second = 12;
}
private:
int _hour;
int _minute;
int _second;
};
clase Date
{
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
1.没有返回值
2.对象实例化时编译器自己调用
3.名字和类名相同
4.可以重载
5.如果类中没有显示定义构造函数,编译器会自动生成一个无参的构造函数,如果显示定义了构造函数,那么编译器不会再生成构造函数。
也就是说,如果显示定义的是一个有参数的构造函数,但是实例化时,写的是一个无参的,没给它传参。由于编译器不会再生成一个构造函数了,现有的构造函数只满足有参的情况,没有合适的无参的构造函数可以使用,所以此时编译器会报错。
6.无参的构造函数和全缺省的构造函数都是默认构造函数,并且默认构造函数只能有一个。
三、析构函数
对象在销毁时会调用析构函数,完成对象中的资源清理工作。比如把malloc申请的空间释放。
析构函数也是特殊的成员函数。
特点:
1.析构函数名是在类名前加~
2.无参数,无返回值类型
3.一个类只有一个析构函数。若没有显示定义,编译器会自动生成默认析构函数。析构函数不能重载。
4.对象生命周期结束时,编译器自动调用析构函数。
后申请的资源先析构。
5.编译器生成的默认析构函数,对自定义成员会调用它的析构函数。对于内置类型销毁时不需要清理资源,最后系统直接回收就可以。
6.没有资源申请时,可以不写析构函数,用编译器自己生成的。有资源申请时,一定要写,为了防止内存泄漏。
四、拷贝构造函数
只有单个形参,通常是const修饰的,是对对象的引用。这个形参用来给需要新创建的对象进行复制,在用已存在的 类类型对象 创建新对象时 由编译器自动调用。
clase Date
{
public:
Date(int year = 1900, month = 1, day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数,天选之子之一。
Date(const Date& d)//这里要用传引用,否则会无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
int main()
{
Date d(1,2,3);
Date d2(d);
return 0;
}
特点:
1.拷贝构造是构造函数的一个重载形式(他们名字相同,只是参数不同)
2.拷贝构造函数的参数只有一个,且必须是类类型对象的引用,使用传值方式编译器会直接报错。因为会引发无穷递归调用。
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数会以字节序完成拷贝。这是浅拷贝,或者值拷贝。
编译器对于内置类型的拷贝是以字节方式直接拷贝的,对于自定义类型的拷贝是调用该自定义类型的拷贝构造函数完成拷贝的。
4.拷贝构造函数在一些场景下,比如申请资源后,必须用到深拷贝。否则浅拷贝会拷贝出同一个地址,这样两个对象就用同一块空间了(比如银行里存了15万,另一个人存了200,因为这个人存了200,我的账户就跟着变成200了,这显然是重大的损失。)。深拷贝要自己写,重新申请一块空间。进行拷贝。
5.拷贝构造函数典型调用场景:
a 使用已存在的对象创建新对象
b 函数参数类型为类类型对象
c 函数返回值类型为类类型对象
五、赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。有 返回类型 参数列表。它的返回类型和参数列表与普通函数相似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型: 返回值类型 operator操作符(参数列表)
注意:
a 不能通过连接其他符号来创建新的操作符: 比如operator$
b 重载操作符必须有一个类类型参数
c 作为类成员函数重载时,形参看起来比操作数数目少1,因为成员函数的第一个形参是隐藏的this
d .* :: sizeof ?: . 这五个运算符不能重载。
clase Date
{
friend bool operator==(const Date& d1 , const Date& d2);//友元函数
public:
Date(int year = 1900, month = 1, day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数,天选之子之一。
Date(const Date& d)//这里要用传引用,否则会无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
bool operator==(const Date& d1 , const Date& d2) //全局的
{
return d1._year==d2._year && d1._month==d2._month && d1._day == d2._day;
}
int main()
{
Date d(1,2,3);
Date d2(1,2,3);
cout << (d1==d2) <<endl;
return 0;
}
这是全局的运算符重载,用全局的operator有一个很明显的问题,就是访问限制。它不能访问Date里面的内置类型。所以在Date里面加了一个friend 后面添加了这个函数的声明,表示这个函数是朋友,就是友元函数,允许它访问类里的成员。
clase Date
{
public:
Date(int year = 1900, month = 1, day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数,天选之子之一。
Date(const Date& d)//这里要用传引用,否则会无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Date& d1) //这里的d1是第二个参数,第一个参数是this
{
return _year==d1._year && _month==d1._month && _day == d1._day;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
int main()
{
Date d(1,2,3);
Date d2(1,2,3);
cout << (d1==d2) <<endl;
return 0;
}
上面那个代码是把运算符重载放在了类里面,隐藏了第一个this指针。
赋值运算符重载:
参数类型:constT&,引用传递可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
检测是否自己给自己赋值
返回*this:要复合连续赋值的含义。
clase Date
{
public:
Date(int year = 1900, month = 1, 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;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
1.和运算符重载不一样,赋值运算符重载,只能重载成类的成员函数,不能重载为全局函数。
因为赋值运算符是天选之子,如果不在类里显示定义,而在全局定义。编译器就会自己生成一个默认的赋值运算符重载,这样就发生冲突了。编译器不知道该调用哪个。所以赋值运算符只能是类的成员函数。
2.用户没有显式定义时,编译器会生成一个默认赋值运算符重载,以值的方式拷贝。对内置类型是直接赋值,对自定义类型调用该自定义类型的赋值运算符重载来完成赋值。
3.像拷贝构造一样,赋值运算符重载也面临着对申请过资源的情况,必须自己写一个赋值运算符重载,而不能依赖系统给的。否则可能会发生两个不同对象占用同一块空间的尴尬局面。并且一个空间的销毁也会影响另一个空间。
前置++和后置++重载
clase Date
{
friend bool operator==(const Date& d1 , const Date& d2);//友元函数
public:
Date(int year = 1900, month = 1, day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)//这里要用传引用,否则会无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator++()//前置++重载
{
_day+=1;
return *this;
}
Date operator(int)//后置++要加一个int型参数
{
Date tmp(*this);//拷贝运算符重载
_day+=1;
return tmp; //tmp是临时对象,不能返回引用,只能返回值
}//它自己变了,但它返回的值还是传来的样子
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
int main()
{
Date d;
Date d2(1,1,2);
d = d2++;//d 1,1,2 d2 1,1,3
d = ++d2;//d2 1,1,4 d 1,1,4
return 0;
}
六、const成员函数
const修饰的“成员函数”称为“const成员函数”。
const修饰类成员函数,实际上修饰的是this指针,表明在该成员函数中,不能对类的任何成员进行修改。
clase Date
{
public:
Date(int year = 1900, month = 1, day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数,天选之子之一。
Date(const Date& d)//这里要用传引用,否则会无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()const
{
cout<<"_year"<<endl;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
int main()
{
Date d;
d.Print();
return 0;
}
const修饰的成员函数运用时也存在权限放大和缩小问题,权限可以缩小,但不能放大。
const对象可以调用const成员函数,但不能调用非const成员函数。非const对象两者都可以调用。
取地址及const取地址操作符重载
这两个默认成员函数一般不用定义,编译器会默认生成。
clase Date
{
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
//内置类型
int _year=1;//给个默认值
int _month=1;
int _day=1;
};
一般情况下编译器会自动生成不需要重载,只有特殊情况,比如想让用户获取到指定的地址(而不是真实的地址)。
所以最后两个默认成员函数基本是了解就好了。