类的6个默认成员函数
用户没有显式实现,编译器会自动生成的成员函数,称为默认成员函数.
例如下面这个空类,什么都没写,但实际上已经默认生成了6个成员函数.
class EmptyClass
{}
构造函数
引出
当用类实例化一个对象后,一般都要先初始化对象,调用init()之类的函数初始化.
但有时会忘记初始化,导致程序崩溃或出现随机值.
能不能保证用类实例化对象时,对象自动被初始化呢?构造函数横空出世.
特性
构造函数是特殊的成员函数,和普通函数的定义不同、调用规则不同.
(1) 定义不同:
函数名与类名相同
函数无返回值
class Date
{
public:
//函数名与类名相同,无返回值
Date()
{
_year = 2022;
_month = 11;
_day = 14;
}
private:
int _year;
int _month;
int _day;
};
(2) 调用规则不同:
用类实例化对象时,编译器自动调用对应的构造函数,若无匹配则报错
(3) 构造函数可以实现函数重载
class Date
{
public:
Date()//构造函数1
{
_year = 2022;
_month = 11;
_day = 14;
}
Date(int year, int month, int day)//构造函数2
{
this->_year = year;//成员函数内部可以显示使用this,也可以不使用
_month = month;
_day = day;
}
Date(int year, int month)//构造函数3
{
_year = year;
_month = month;
_day = 14;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //调用构造函数1
Date d2(2022, 11, 15);//调用构造函数2
Date d3(2022, 12); //调用构造函数3
return 0;
}
(4) 如果类中没有显式定义构造函数,则c++编译器会自动生成一个无参的默认构造函数
一旦用户显式定义构造函数,编译器将不再生成.
(5) 构造函数不是给对象开空间,而是用来初始化对象的.
默认构造函数
不需要传参就能调用的构造函数.
(1) 我们不写,编译器默认生成的无参构造函数
(2) 自己写的全缺省构造函数
(3) 自己写的无参构造函数
但(2)、(3)不能同时出现,因为调用时会产生歧义
class Date
{
public:
Date()//构造函数1
{
_year = 2022;
_month = 11;
_day = 14;
}
Date(int year = 2022, int month = 12, int day = 14)//构造函数2
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用构造1还是构造2?
return 0;
}
默认生成的无参构造函数
特点:
(1) 内置类型不处理
(int、double、所有指针类型)
(2) 自定义类型去调用自定义类型的默认构造函数
(若自定义类型没有默认构造函数就报错)
作用:
有些类的成员变量全是自定义类型,就不需要显式写构造函数,
默认生成的就够用,前提是那个自定义类型的构造函数写好了.
小结:
(1) 一般的类都不会用编译器默认生成的构造函数,
都会自己写,最好写一个全缺省的构造函数.
(2) 建议每个类都要有默认构造函数.
初始化列表【重点】
引出
c++11针对内置类型成员不初始化的缺陷,打了一个补丁.
允许在成员变量声明的地方给缺省值.
class Date
{
public:
Date(){}
private:
int _year = 2022;//这里是声明同时给缺省值,不是定义
int _month = 11;
int _day = 14;
};
针对c++11给成员变量缺省值的新特性,我们来研究这个缺省值什么时候起效果.
class Date
{
public:
//无论是否显式写构造函数,d1均被初始化为2022 11 14
//Date(int year = 1, int month = 1, int day = 1){}
int GetYear() {return _year;}
int GetMonth() {return _month;}
int GetDay() {return _day;}
private:
int _year = 2022;
int _month = 11;
int _day = 14;
};
int main()
{
Date d1;
cout << d1.GetYear() << " " << d1.GetMonth() << " " << d1.GetDay() << endl;
return 0;
}
现象:
1 无论是否显式写构造函数,该缺省值都会起效
2 若显示写构造函数,且在函数体内修改了成员变量,最后对象的结果才不是缺省值
但通过调试发现,当刚进入函数体内时,对象已经用缺省值进行初始化了.
(*this就是当前要初始化的对象)
class Date
{
public:
Date()
{
_year = 999;
_month = 1;
_day = 1;
}
int GetYear() {return _year;}
int GetMonth() {return _month;}
int GetDay() {return _day;}
private:
int _year = 2022;
int _month = 11;
int _day = 14;
};
int main()
{
Date d1;
cout << d1.GetYear() << " " << d1.GetMonth() << " " << d1.GetDay() << endl;
return 0;
}
而真正使用这个缺省值的位置,就是初始化列表.
格式
以一个冒号开始,逗号分隔,每一个成员变量后跟一个放在括号中的初始值或表达式.
Date()
:_year(29)
,_month(11)
,_day(15)
{
//函数体
}
若没有显式使用初始化列表初始化某个成员变量,列表就会使用缺省值.
内置类型成员变量若没有缺省值,就是随机值.
特性
(1) 每个构造函数都有一个初始化列表.(无论是否显式写)
因为实例化对象只会调用一个匹配的构造函数.
Date()
:_year(29)
,_month(11)
,_day(15)
{}
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
(2) 每个成员变量在初始化列表中只能出现一次.(初始化只能初始化一次)
(3) 初始化列表是成员变量初始化的地方,每个成员变量都要走一遍初始化列表.
对于内置类型成员:
没有给缺省值,没有在初始化列表中显式给值,不做处理,仍然是随机值
给了缺省值,没有在初始化列表中显式给值,就会用这个缺省值
如果在初始化列表中显式给值,缺省值不起作用.
对于自定义类型成员:
没有在初始化列表中显式调用构造函数初始化,编译器自动调用自定义类型的默认构造函数
如果显式调用,自定义类型的默认构造不起作用
class Position
{
public:
Position()//默认构造
:_x(99)
,_y(100)
{}
Position(int x)//构造函数2
:_x(x)
,_y(0)
{}
private:
int _x;
int _y;
};
class A
{
public:
A(int a,int b, int x)
:_pos(x)//显示调用自定义类型的构造函数2
,_a(a) //初始化列表显式初始化a,此时缺省值无效
,_b(b)
{}
private:
int _a = 1;
int _b;
Position _pos;
};
(4) 必须放在初始化列表进行初始化的成员:
引用类型成员变量(只有一次初始化的机会)
const成员变量(只有一次初始化的机会)
没有默认构造函数的自定义类型成员
(如果不显式用初始化列表初始化自定义类型成员,就会去自动调用它的默认构造函数,
但该类型没有默认构造函数,就会报错)
class Position
{
public:
Position(int x, int y)
:_x(x)
,_y(y)
{}
private:
int _x;
int _y;
};
class A
{
public:
A(int& a, int con = 1, int x = 1, int y = 1)
:_con(con)//const成员
,_quote(a)//引用成员
,_pos(x, y)//没有默认构造的自定义类型成员,只能在初始化列表中手动调用
{}
private:
const int _con;
int& _quote;
Position _pos;
};
(5) 初始化列表初始化成员变量的顺序和声明顺序一致,与手动初始化的顺序无关.
class Date
{
public:
Date(int month)
:_month(month) //初始化列表初始化的顺序也是_year、_month
,_year(_month)
{}
private:
int _year;//声明顺序是_year 、_month
int _month;
};
int main()
{
Date d1(3);
return 0;
}
小结
自定义类型成员、内置类型成员都推荐使用初始化列表初始化.
(1) 自定义类型成员如果不手动在初始化列表初始化,
编译器仍然会在初始化列表自动调用自定义类型的默认构造函数(没有则报错)
(2) 内置类型成员如果不手动在初始化列表初始化,
编译器会使用声明时给的缺省值(没有给缺省值就为随机值).
(3) const类型成员、引用类型成员、没有默认构造函数的自定义类型成员
必须手动用初始化列表初始化.
析构函数
作用
对象在生命周期结束后会销毁,此时编译器会自动调用析构函数,完成对象内部资源的清理工作.
(一般如果该类的构造函数有在堆上动态开辟空间,就需要在析构函数里手动释放空间)
特性
(1) 析构函数名是在类名前加上~,例:~Stack()
(2) 无参无返回值
(3) 一个类只有一个析构函数,如果没有显式定义,就会生成默认的析构函数
【默认的析构函数对内置类型不处理,自定义类型会调用它的析构函数】
(4) 对象生命周期结束,编译器自动调用析构函数清理空间
~Date(){}//没有资源需要清理
注意
在同一栈帧中的类对象,后定义的先析构;
同在静态区的所有对象,后定义的先析构;
栈区对象的析构一定比静态区对象的析构要快.
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A构造" << _a << endl;
}
~A()
{
cout << "A析构" << _a << endl;
}
private:
int _a;
};
A a3(3);
A a4(4);
int main()
{
A a1(1);
A a2(2);
static A a5(5);
cout << endl;
return 0;
}
小结
(1) 析构函数用来完成对象内部资源的清理工作;
(2) 默认生成析构函数对内置类型不处理,自定义类型会调用它的析构函数,
如果一个类只有自定义类型 或者 没有资源需要清理,就不用显式写析构函数,
例如:
Time类/Date类【没有资源需要清理】
用2个栈实现的Queue【只有自定义类型】
拷贝构造函数
引入
创建对象时,创建一个与已存在对象一模一样的新对象,例:
int i = 0;
int j = i;//用i拷贝构造出j
想要让自定义类也能实现以上功能,于是出现了拷贝构造函数。
Stack st1;
Stack st2(st1);//兼容c,也可以Stack st2 = st1
Date d1(2023, 8, 2);
Date d2(d1);//Date d2 = d1
特征
(1)它是构造函数的一个重载形式,它也是构造函数
(2)它只有一个形参,该形参必须是本类型的引用(后面重点)
(3) 在用【已存在同类对象】创建新对象时,编译器会自动调用拷贝构造函数
(4) 不显式写,编译器自动生成,内置类型会按字节逐个拷贝,自定义类型调用其拷贝构造.
以Date类为例:
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d1;
Date d2(d1);//自动调用拷贝构造函数
为什么该形参必须是本类型的引用?
若是传值传参:
调用拷贝构造函数时,形参是实参的拷贝,传参会再次调用拷贝构造,就会引发无穷递归.
该形参一般用const修饰
一方面,防止代码写错,造成已有对象被修改.
另一个点是可以用const类型对象进行拷贝构造,不用const修饰形参会造成权限的缩小
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
const Date d1;
Date d2(d1);
小结
(1) 用已有的类对象创建新对象时,会调用拷贝构造函数
(2) 默认生成的拷贝构造函数对内置类型不处理,自定义类型调用其拷贝构造函数,
因此Date类不需要手动写,默认生成的够用
(3) 拷贝构造函数的形参必须是同类型对象的引用,同时最好用const修饰.
(4) 像Stack类,必须要自己写拷贝构造函数,
因为按字节拷贝会导致2个栈对象内部的指针指向同一个空间 —— 同一空间析构2次
另外一个栈修改还会影响另一个栈.
class Stack
{
public:
Stack(int capacity = 5)
:_top(0)
,_capacity(capacity)
{
arr = (int*)calloc(_capacity, sizeof(int));
}
//拷贝构造函数自动生成
void push(int num)
{
if (_top == _capacity)
{
//扩容
int* tmp = (int*)realloc(arr, 2 * _capacity);
_capacity *= 2;
}
arr[_top] = num;
_top++;
}
void pop()
{
if(_top > 0)
_top--;
}
~Stack()
{
free(arr);
}
private:
int _top;
int _capacity;
int* arr;
};
运算符重载
引入
内置类型可以直接使用运算符运算,编译器知道如何运算
int i = 0;
int j = 1;
i++; j--;
i + 10; j - i;
i == j; i > j; i < j;
但自定义类型不能直接使用这些运算符运算。
为了让自定义类型支持使用运算符,引入了运算符重载。
运算符重载本质是拥有特殊函数名的函数
特征
(1) 函数名字:operator接需要重载的运算符符号
例:operator==,operator+,operator++等等,这些都是特殊函数名称
(2) 函数原型:返回值类型 operator操作符(参数列表)
例:bool operator==(Position p1, Position p2);
若有两个参数,左参数是左操作数,右参数是右操作数
参数列表和返回值一般看操作符功能自己选定
(3) 为了能在函数内部直接使用类的成员变量,一般运算符重载都会作为类的成员函数。
类的成员函数内部,只要是该类的对象,都不受访问限定符的限制
class Position
{
public:
Position(int x = 0, int y = 0)
:_x(x)
,_y(y)
{}
//左操作数是隐藏的this指针
//为了减小拷贝,右操作数使用引用传参
//为了右操作数能传const类型,使用const引用
bool operator==(const Position& p1)
{
return p1._x == _x && p1._y == _y;
}
private:
int _x;
int _y;
};
此时调用位置如下:
int main()
{
Position p(3, 5);
Position p1(3, 5);
//p == p1 会被编译器转化为 p.operator==(&p,p1),&p不能显式传
cout << (p == p1);
cout << (p.operator==(p1));
return 0;
}
赋值运算符重载
引入
已经存在的两个自定义类型对象,想要把一个对象的值赋值(拷贝)给另一个对象。例:
int a = 1;
int b = 2;
a = b;//把b拷贝给a
Date d1(2023,8,2);
Date d2(1,1,1);
d2 = d1;//通过调用赋值运算符重载实现
特征
(1) 函数原型:类& operator=(const 类& 名)
例:Date& operator=(const Date& d)
(2) 返回左操作数的引用 —— 为了支持连续赋值,同时出了该函数作用域*this不会销毁
例:a = b = 3;
d1 = d2 = d3;
参数有const修饰,同时是类对象引用 —— 减少拷贝,并且能引用const类型对象
Date& operator=(const Date& d)
{
//内部完成值拷贝
return *this;
}
Date d1(2023, 8, 2);
Date d2(2023, 8, 3);
d2 = d1;//编译器会转化为d2.operator(&d2, d1)
(3) 类里自己不写,编译器会默认生成赋值运算符重载。
生成的赋值运算符重载,内置类型成员按字节逐个赋值,自定义类型成员调用其赋值运算符重载
Date类的运算符重载
(1) 日期 += 天数
同时用日期 += 天数 实现 日期 + 天数
天数不合法,向下一个月份进位,同时减去本月的所有天数,代表这个月过完了
//获取该月的天数
int GetMonthDay(int year, int month)
{
//声明为static,不需要重复创建该数组
static int ret[13] = { 0, 31, 28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 &&
((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))
)
{
return 29;
}
return ret[month];
}
Date& operator+=(int day)//日期加天数,计算day天后的日期是多少
{
_day += day;//先累加天数,若天数合法直接返回,不合法就进月,月份超出进年
//当前月的最大天数
int maxDay = GetMonthDay(_year, _month);
//天数不合法
while(_day > maxDay)
{
_day -= maxDay;
_month++;
//月份超出
if (_month == 13)
{
_month = 1;
_year++;
}
maxDay = GetMonthDay(_year, _month);
}
//因为*this出了该函数作用域后不销毁,所以采用引用返回,减少拷贝
return *this;
}
//d1 + 10 是不会改变d1的
Date operator+(int day)
{
//拷贝构造一份ret,不能改变*this的内容
Date ret = *this;
//复用+=
ret += day;
//ret出了函数作用域,就销毁了,必须传值返回
return ret;
}
(2) 日期 -= 天数
同时用日期 -= 天数 实现 日期-天数
天数不合法,需要向上一个月份借天数
//日期 -= 天数,计算day天前的日期
Date& operator-=(int day)
{
_day -= day;
//若天数不合法,需要向上一个月份借天数
while (_day <= 0)
{
_month--;
//月不够就向年借
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//日期-天数
Date operator-(int day)
{
Date ret = *this;
ret -= day;
return ret;
}
(3) 前置++和后置++的运算符重载(--类似)
如果直接按特性重载该运算符,调用时无法区分;
所以c++特殊处理,后置++的重载增加一个int参数,跟前置构成函数重载区分
Date& operator++();//前置,没有参数
Date operator++(int);//后置,加一个int参数
++d1;//编译器转化为 d1.operator++(&d1);
d1++;//编译器转化为 d1.operator++(&d1,0),整数传什么不重要
具体实现:
前置++返回+之后的值,把this+=1后,直接返回this;(可以用引用返回)
后置++返回+之前的值,先创建对象tmp保存先前的值,再对*this+=1,返回tmp(只能传值返回)
//前置++
Date& operator++()
{
return *this += 1;
}
//后置++
Date operator++(int)
{
Date tmp = *this;
++(*this);
return tmp;
}
补充
【.*】 、【 ::】 、【sizeof 】、【?:】 、【.】
以上5个运算符不能重载
const成员函数
什么是const成员函数
const修饰的成员函数就是const成员函数
本质是修饰该成员函数隐藏的形参this
例:
bool operator==(const Date&d)
{
//比较逻辑
}
//此时隐藏的this指针—— Date* const this
this指针的指向不能变,但是指向的内容仍可以修改
为了万无一失,如何才能让this指向的内容在该函数内不可修改呢?
bool operator==(const Date&d)const//const成员函数
{}
//此时this指针 —— const Date* const this
用const修饰,表示在该成员函数中不能对this的任何成员作修改
作用
const类对象能调用const成员函数,无法调用普通成员函数,因为会导致权限的扩大
const Date d1(2023, 8, 2);
d1.print();//编译器会转化为d1.print(&d1) 即const Date* 类型
void print()//此时的this类型:Date* const this
{
cout << _year << " " << _month << " " << _day << endl;
}
此时要把print设为const成员函数才能让const类对象正常调用
建议:成员函数内部如果不需要改变this指针指向的内容,都应该作为const成员函数
普通类对象可以调用,const类对象也可以调用.
取地址重载及const取地址重载
一般不用自己定义,编译器默认会生成
const Date* operator&()const
{
cout << "&const" << endl;
return this;
}
Date* operator&()
{
cout << "&" << endl;
return this;
}
const Date d1(2023, 7, 1);
Date d2(2023, 8, 1);
&d1;
&d2;
它们会构成函数重载,一个是const Date*const this,
另一个是Date* const this
explicit关键字
引入
只需传一个参数就能调用的构造函数,具有类型转换作用
class A
{
public:
A(int a,int b = 1)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
int main()
{
//先用10构造出临时对象tmp(隐式类型转换),再用tmp拷贝构造a ——> 编译器优化为 直接构造
A a = 10;
A a(10);//直接调用构造,没有发生隐式类型转换
return 0;
}
作用
修饰构造函数,禁止隐式类型转换发生
static成员变量与成员函数
static成员变量
用static修饰的成员变量为静态成员变量.
特征
(1) 必须在类外进行初始化,无法用初始化列表初始化静态成员变量.
class A
{
public:A(){}
private:
static int _st;//仅仅是声明
};
//在类外初始化,用::指定类域
int A::_st = 0;
(2) 存储在静态区,程序刚开始运行就一直存在.
(3) 所有的类对象共用 同一个 static 成员变量【重点】
因为_st被设置为private,除定义时外,类外不能用A::_st的方式访问
class A
{
public:
A()
{
//该类每创建出一个新对象,就++_st,最后可以统计用该类一共创建了多少对象
_st++;
}
//获取_st的值
int GetStaticVal()
{
return _st;
}
private:
static int _st;
};
int A::_st = 0;
int main()
{
A a1;
A a2;
A a3;
cout << a1.GetStaticVal() << endl;
cout << a2.GetStaticVal() << endl;
cout << a3.GetStaticVal() << endl;
return 0;
}
(4) 由于静态成员变量存储在静态区,类计算大小不计算它
(5) 静态成员变量在类外面:
可以用对象.静态成员变量来访问
或者用类域::静态成员变量来访问
但仍受到访问限定符的限制
static成员函数
用static修饰的成员函数为静态成员函数.
特征
(1) 静态成员函数在类外(与静态成员变量一致):
可以用对象.静态成员函数来访问
或者用类域::静态成员函数来访问【有些场景下不需要再专门创建对象,来访问该函数】
但仍受到访问限定符的限制
(2) 静态成员函数没有this指针,无法访问非静态成员,可以访问静态成员【重点】
注意:这里的静态成员包括 静态成员函数 和 静态成员变量 .
本质上是 普通的成员函数,在访问成员变量和其他成员函数时,前面都会默认加上this->,
非静态的成员函数隐藏的this指针可以帮助访问成员变量和其他成员函数,
失去了this指针就只能访问静态的成员变量和成员函数
(3) 非静态成员函数可以访问静态成员函数或静态成员变量
统计一个类创建了多少对象
class A
{
public:
A()//构造
{
//每进入一次构造函数,说明创建了一个对象
++_st;
}
A(const A& a)//拷贝构造
{
++_st;
}
static void printNum()//声明为static,不需要创建任何对象,都能用A::调用
{
cout << "目前创建A类对象数目:" << _st << endl;
}
private:
static int _st;
};
int A::_st = 0;
int main()
{
A::printNum();
A a1; A a2; A a3;
A::printNum();
return 0;
}
友元函数/友元类
友元函数
友元函数是定义在类外的普通函数,【不属于任何类】,需要在类的内部声明,
声明时加friend关键字.例:
class A
{
//可以放在类内任意地方,不受访问限定符限制
friend void func();
public:
A()
:_a(1)
,_b(1)
{}
private:
void print(){};
int _a;
int _b;
};
void func()//友元函数
{
//由于func是A类的友元函数,内部可以用A的对象直接访问私有/保护成员
A a;
a.print();
cout << a._a << " " << a._b << endl;
}
int main()
{
func();
return 0;
}
注意
(1) 友元函数内部,可以用类对象直接访问类的私有/保护成员.
但它不是类的成员函数,也没有this指针!!!
(2) 友元函数不能有const修饰,const是修饰this指向的内容,但友元函数没有this指针
(3) 一个函数可以是多个类的友元函数
void func(const A& a, const B& b, const C& c);
此时可以考虑把func作为A、B、C类的友元函数.
友元类
友元类的所有成员函数都可以直接访问另一个类的私有成员函数/变量.
class A
{
friend class B;//B是A的友元类
public:
A():_a(1),_b(1){}
void APrint() {};
static void APublicStatic() {};
private:
static void APrivateStatic() {};
int _a;int _b;
};
class B
{
public:
B(){}
void testFriend()
{
A a;
a.APrint();
a.APublicStatic();
a.APrivateStatic();
cout << a._a <<" " << a._b << endl;
}
};
int main()
{
B b;
b.testFriend();
return 0;
}
注意
(1) 友元类是单向性的,B是A的友元类,B里成员函数可以直接访问A的私有成员,
但A不能直接访问B的私有成员.
(2) 友元关系不能传递,例:
A是B的友元类,B是C的友元类,但A不是C的友元.
(3)友元破坏了封装,增加了程序之间的耦合度,尽量少用.
内部类
一个类定义在另一个类的内部,这个处于内部的类就是内部类。
class A
{
public:
class B//此时B就是内部类
{
private:
double _b;
};
private:
int _a;
};
特征
(1) 内部类B 受 外部类A 的类域 和 访问限定符 限制,
想要使用B类必须先指定A这个类域,同时还必须被public修饰.
//B b; —— 报错
A::B b;
(2) 内部类天生是外部类的友元类,内部类可以直接访问外部类的私有成员.【重点】
class A
{
public:
class B//此时B就是内部类
{
public:
void test()
{
//直接用外部类对象,访问外部类的私有成员
A a;
a._a = 1;
cout << "_a修改成功:" << a._a << endl;
}
};
private:
int _a;
};
int main()
{
A::B b;
b.test();
return 0;
}
(3) 内部类和普通类计算大小没有区别,A类的大小与B无关,B类的大小与A无关