一.类的默认成员函数
默认成员函数是什么?默认成员函数是用户没有显示实现,由编译器自动生成的成员函数。在类中默认生成的成员函数有6个,如下:
二.构造函数
构造函数不是普通的成员函数,虽然名字是“构造”,但并不是担任开辟空间的作用,而是起着初始化对象的作用,开辟空间是在创建栈帧时就已经开辟好的,构造函数就是起到了类似于栈和队列初始化函数的作用,并且构造函数是自动调用的。
构造函数的特点:
1.函数名与类名相同。
2.构造函数无返回值。(这里的无返回值是指,连void也不需要写,C++规定)
3.在类实例化时自动调用。
#include<iostream>
using namespace std;
class Date
{
public:
//函数名与类名相同,无返回值
Date()
{
_year = 2024;
_month = 7;
_year = 16;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//自动调用
Date d;
return 0;
}
4.可以重载
5.如果用户不显示实现,编译器会自动生成一个无参的构造函数,
6.默认构造函数是有3个:无参构造函数、全缺省构造函数、编译器自己生成的无参构造函数。注意!经常会认为默认构造函数只有编译器自己生成的无参构造函数才是默认构造函数,但不是!3个默认构造函数在一个类中只能存在一个,不能同时存在。首先自己实现了构造函数,编译器的就不会再另外生成,无参构造函数与全缺省构造函数属于函数重载,在调用时会出现歧义,不知道调用哪一个。
#include<iostream>
using namespace std;
class Date
{
public:
//无参
//Date()
//{
// _year = 2024;
// _month = 7;
// _day = 16;
//}
//全缺省
/*Date(int year=1,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;
//d1.print();
//全缺省构造函数调用
//Date d2(2024,7,16);
//d2.print();
//自动生成的构造函数,vs2019下是随机值
Date d3;
d3.print();
return 0;
}
7.编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化(例如用两个栈实现队列)。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表。
ps:内置类型就是int,double,float等等,自定义类型是指用class或者struck创建的新类型。
#include<iostream>
using namespace std;
class stack
{
public:
stack(int n = 4)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
private:
int* _a;
int _top;
int _capacity;
};
class myqueue
{
public:
private:
stack stack1;
stack stack2;
};
int main()
{
//queue调用自动生成的构造函数,在自动生成的构造函数里,又调用了自定义类型数据的stack的构造函数
myqueue queue;
return 0;
}
三.析构函数
析构函数与构造函数相反,析构函数不是对对像本身进行销毁,局部对象是存在栈帧的,当局部函数结束,对象也就销毁了,析构函数是在对象销毁后,进行资源的清理。例如是否栈,队列这些申请了资源。所以对于Date这种全是内置类成员的类,是可以不需要析构函数的。
析构函数特点:
1.函数名是在类名前加一个~。
2.函数无返回值,与构造函数相同。
3.一个类只有一个析构函数,若用户没有显示实现,则编译器自动生成。
4.. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6.自定义类型一定会调用析构函数
7.如果类中有无资源的申请,那么可以不用自己实现,编译器自动生成的就可以用;当类中有资源申请时,则需要自己实现析构函数,否则会造成内存泄漏,如stack;像myqueue这种,它自己生成的析构函数会,自动调用stack的析构函数。
8.一个局部域有多个对象,先析构后定义的对象。
#include<iostream>
using namespace std;
class stack
{
public:
stack(int n = 4)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = n;
}
~stack()
{
cout << "~stack()" << endl;
free(_a);
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class myqueue
{
public:
//无论用户是否实现析构函数,都会调用stack的析构函数
/*~myqueue()
{
}*/
private:
stack stack1;
stack stack2;
};
int main()
{
myqueue queue;
return 0;
}
四.拷贝构造函数
拷贝构造函数就是特殊的构造函数
拷贝构造函数特点:
1.拷贝构造函数是构造函数的重载。
2.拷贝构造函数的参数只有一个,是自身类型的引用,如果使用传值传参,系统崩溃。
3.C++规定自定义类型对象进行拷贝行为时必须调用拷贝构造,所以传值传参和传值返回都需要进行拷贝构造。
4.用户未自己生成拷贝构造函数时,会自动生成,这个只是浅拷贝(一个字节一个字节的拷贝),为申请资源还好,如果类型申请了资源就会出现问题。对自定义类型成员会调用拷贝构造。
#include<iostream>
using namespace std;
class Date
{
public:
//与构造函数形成函数重载,第一个参数是自身类型的引用
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//自动调用
Date d1(2024, 7, 16);
d1.print();
//两种拷贝构造函数的调用
Date d2 = d1;
//Date d2(d1);
d2.print();
return 0;
}
5.像Date这种全是内置类型且未申请资源的类,编译器自动生成的拷贝构造就足够满足用户要求,但是像stack这类虽然都是内置类型但有资源申请的类,就不能使用编译器自动生成的拷贝构造函数了,需要用户自己生成拷贝构造进行深拷贝(申请的资源的指向也相同)。然后像由两个栈实现的队列的myqueue类,会自己调用stack的拷贝构造。
#include<iostream>
using namespace std;
class stack
{
public:
//构造函数
stack(int n = 4)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = n;
}
//拷贝构造函数
stack(const stack&s)
{
_a = (int*)malloc(sizeof(int)*s._capacity);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a,s._a, sizeof(int) * s._capacity);
_top = s._top;
_capacity = s._capacity;
}
void Push(int x)
{
if (_top == _capacity)
{
int Newcapacity = 2 * _capacity;
_a = (int*)realloc(_a, sizeof(int) * Newcapacity);
if (_a == nullptr)
{
perror("realloc fail!");
return;
}
_capacity = Newcapacity;
}
_a[_top] = x;
_top++;
}
//析构函数
~stack()
{
cout << "~stack()" << endl;
free(_a);
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
class myqueue
{
public:
//无论用户是否实现析构函数,都会调用stack的析构函数
/*~myqueue()
{
}*/
private:
stack stack1;
stack stack2;
};
int main()
{
/*myqueue queue;*/
stack stl1;
stl1.Push(1);
stl1.Push(2);
stl1.Push(3);
//这里如果是浅拷贝的话,stl1和stl2的_a指向的地址是同一个,会析构两次。
stack stl2(stl1);
return 0;
}
6.返回值可以是传值返回,也可以是传引用返回。传值返回会进行拷贝构造,传引用返回是直接返回的别名。但是!要考略这个引用对象的生命周期,如果这个对象是在局部类定义的,当局部函数结束,栈帧销毁,这个时候再使用传引用返回就会出现错误,即野引用的情况。
五.赋值运算符重载
5.1运算符重载
1.当类类型使用运算符时,C++允许我们使用运算符重载的形式来赋予新的含义。对对象使用运算符时就会调用运算符重载函数。
2.加入一个关键字operator,如要重载+,则是operator+,运算符重载函数有返回值,根据情况来定。
3.参数列表与重载的运算符的作用个数一样,如+是二元运算符,则参数表就两个。
4.当运算符重载为成员函数时,因为第一个参数是隐式的this指针,所以成员函数要少一个参数。
5.语句块类内不可以改变运算符的优先性和结合性。
6.不能对没有的符号进行重载,如operator@。
7. :: 、 sizeof 、? : 、 . 、.* ,这五个运算符不可以重载。
8.需要重载的是一些对类有意义的运算符,如Date类,日期-日期是中间的差值,日期+日期就没有意义,就不需要重载。
9.前置++,和后置++都可以重载,函数名都是operator++,怎么区分呢?规定在后置++的参数表多一个int类型,用以区分前置++。
10.重载>>和<<时,需要重载为全局函数,因为重载为成员函数, this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
重载为全局的⾯临对象访问私有成员变量的问题 , 有⼏种⽅法可以解决:
1 .成员放公有(类外可以直接访问了)
2 . Date 提供 getxxx 函数(可以)
3 .友元函数
4 .重载为成员函数(最好)
5.2赋值运算符重载
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟 拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
复制构造函数的特点:
1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引⽤,否则会传值传参会有拷⻉
2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋 值场景。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& 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;
};
int main()
{
//自动调用
Date d1(2024,7,16);
d1.print();
Date d2(1, 1, 1);
d2.print();
d2 = d1;
d2.print();
Date d3(2,2,2);
d3.print();
Date d4(3,3,3);
d4.print();
d3 = d4=d1;
d3.print();
d4.print();
return 0;
}
3.用户未自己生成运算符重载函数时,编译器会自动生成,与拷贝构造类似,这个只是浅拷贝(一个字节一个字节的拷贝),为申请资源还好,如果类型申请了资源就会出现问题。对自定义类型成员会调用拷贝构造。
5.3日期类实现
#include"Date.h"
void test1()
{
Date date1(2024, 7, 13);
//date1 -= 300;
date1 +=-30;
date1.print();
}
void test2()
{
Date d2(2024, 7, 13);
Date d3(2024, 7, 13);
bool ret=d2<=d3;
cout << ret << endl;
}
int main()
{
test1();
return 0;
}
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year, int month, int day);
void print();
int Getmonth_day(int year,int month)const
{
assert(month > 0 && month < 13);
static int monthday[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
{
return 29;
}
return monthday[month];
}
bool check();
bool operator<(const Date& date)const;
bool operator<=(const Date& date)const;
bool operator>(const Date& date)const;
bool operator>=(const Date& date)const;
bool operator==(const Date& date)const;
bool operator!=(const Date& date)const;
//日期加减天数
Date operator+(int day);
Date operator-(int day);
Date& operator+=(int day);
Date& operator-=(int day);
//自定义流输入输出
};
#include"Date.h"
Date::Date(int year=2000, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
if (!check())
{
cout << "非法日期:";
print();
}
}
bool Date::check()
{
if (_month < 1 || _month>12 || _day<0 || _day>Getmonth_day(_year,_month))
{
return false;
}
return true;
}
void Date::print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date Date:: operator+(int day)
{
Date temp = *this;
temp += day;
return temp;
}
Date& Date:: operator+=(int day)
{
if (day < 0)
{
*this -= (-day);
return *this;
}
_day += day;
while (_day > Getmonth_day(_year,_month))
{
_day -= Getmonth_day(_year,_month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date:: operator-(int day)
{
Date temp = *this;
temp -= day;
return temp;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <=0)
{
_month--;
if (_month==0)
{
_year--;
_month = 12;
}
_day += Getmonth_day(_year, _month);
}
return *this;
}
bool Date:: operator<(const Date& date)const
{
if (_year < date._year)
{
return true;
}
else if(_year==date._year)
{
if (_month < date._month)
{
return true;
}
else if (_month == date._month)
{
return _day < date._day;
}
}
return false;
}
bool Date::operator<=(const Date& date)const
{
return *this < date || *this == date;
}
bool Date:: operator==(const Date& date)const
{
return _year == date._year
&& _month == date._month
&& _day == date._day;
}
bool Date::operator>(const Date& date)const
{
return !(*this < date);
}
bool Date::operator>=(const Date& date)const
{
return *this > date || *this == date;
}
bool Date::operator!=(const Date& date)const
{
return !(*this == date);
}
六.取地址运算符重载
6.1const成员函数
1.将const修饰的成员函数称之为const成员函数,因为this是隐式的,const修饰成员函数放到成员函数参数列表的后⾯。
2. const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this变为cosnt Date* const this。
在上面实现Date类中有用到。
6.2取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
class Date
{
public :
const Date* operator&()const
{
return 0x90ff44;//(随便给,这样就取不到真正的地址了)
}
private :
int _year ;
int _month ;
int _day ;
};