目录
1.类的默认成员函数
Q:什么是类的默认成员函数?
A:如果用户自己没有显式实现,编译器会自动生成的成员函数被称为默认成员函数。
事实上,除了图片里提到的6种,c++11中还增加
了移动构造和移动赋值两个默认成员函数
从两个方面来学习重要且比较默认成员函数我认为比较好
- 我们不显式实现时,编译器默认生成的函数行为是什么?够不够用?
- 如果编译器默认生成的不满足需求的话,我们怎么显式实现一个能满足我们需求的默认成员函数?
构造函数
构造函数是类的默认成员函数之一,它的主要任务是为实例化的对象初始化。
特点:
- 函数名与类相同
- 无返回值(自然也不用写函数类型)
- 对象实例化时系统会自动调用对应的的构造函数
- 可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显
式定义编译器将不再生成。 - 默认构造函数:总之是不用传实参就可以调用的构造函数,包括编译器默认生成的构造函数,显式实现的无参构造函数,全缺省构造函数三种。它们最多只能同时存在一个。
- 编译器默认生成的构造函数不会管内置类型成员变量,会去调用自定义类型成员变量自己的构造函数。
而且,没有使用“初始化列表”而是赋值的普通构造函数无法初始化引用成员变量,const成员变量,没有默认构造的类类型变量,会编译报错。
class Date
{
public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全缺省构造函数
/*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;
// 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
// 区分这⾥是函数声明还是实例化对象
// warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?
Date d3();//这种调用无参构造的方法是错误的
};
构造函数的初始化列表
上文提到,引用成员变量,const成员变量,没有默认构造的类类型变量或是不想使用类类型成员变量自己的构造函数的情况下,只能使用初始化列表的构造函数来初始化实例化后对象的成员变量。
- 初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
- C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的
成员使用的。 - 这两种初始化列表使用方法最好不要混用。
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)//一个带有初始化列表的全缺省构造函数
:_year(year)
,_month(month)
,_day(day)
,_t(12)
,_ref(x)
,_n(1)
{}
private:
//int _year = year;给缺省值的初始化列表的初始化方法
int _year;
int _month;
int _day;
Time _t; // 没有默认构造
int& _ref; // 引⽤
const int _n; // const
};
- 一旦使用了初始化列表构造函数,所有变量都会走初始化列表这条路线,所以所有变量最好都用初始化列表的方法来初始化。
- 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义
初始化的地方。 - 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无
关。建议声明顺序和初始化列表顺序保持一致。 - 无论是否显式写初始化列表,每个构造函数都有初始化列表;
- 初始化列表也可以用来给需要资源的成员变量开空间
析构函数
析构函数是类的默认成员函数之一,它的主要任务与构造函数相反,是为了完成对对象本身的销毁(释放资源)。
特点:
- 析构函数名为~ + 类名(~Date)
- 无参数无返回值 (跟构造类似)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,系统会自动调用析构函数。
- 编译器默认生成的析构函数不会管内置类型成员变量,会去调用自定义类型成员变量自己的析构函数(和构造相似)
- 自定义类型成员无论什么情况都会自动调用它自己的析构函数。
- 当类中申请资源,比如开辟内存的时候,就要自己实现一个析构来释放资源。
- 一个局部域的多个对象,C++规定后定义的先析构。
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数
也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
特点:
- 拷贝构造函数是构造函数的一个特殊重载。
- 第一个参数必须是自身类类型的引用,因为传值调用和传值返回都会涉及拷贝,cpp规定类类型对象的拷贝必须调用拷贝构造函数,会造成无穷递归死循环。所以不能传值调用。
- 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成
员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
重点:
- 如果一个类里没有成员变量指向资源,浅拷贝就够用了。
- 但是如果有一个指针类型的变量指向了一块内存空间的话,无法拷贝内存空间的浅拷贝显然是满足不了我们的需求的,我们这时候就需要手动实现一个拷贝构造函数来进行可以拷贝内存空间的深拷贝了。
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
// 需要对_a指向资源创建同样⼤的资源再拷贝值
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
//Stack不显⽰实现拷贝构造,⽤⾃动⽣成的拷贝构造完成浅拷贝
// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
- 所以看拷贝构造需不需要实现与否,逻辑上是和析构函数同步的。
2.赋值运算符重载
运算符重载
运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和普通函数一样,它也具有其返回类型和参数列表以及函数体。
特点:
-
想要在类类型,即自定义类型中使用运算符的话,c++允许我们通过运算符重载的形式为运算符提供运算方法论,为其指定新的含义(自定义类型对象运算需要自定义运算符)。
-
重载运算符函数的参数个数和该运算符作⽤的运算对象数量一样多。一元运算符有⼀个参数,二元
运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
更多元同理。(只有二元及其以上运算符区分参数方向) -
如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算
符重载作为成员函数时,参数比运算对象少一个。 -
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
-
不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
-
这五个运算符不能重载。
-
重载操作符⾄少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,
如:
int operator+(int x, int y)//没有类类型对象为什么要重载?
- 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。
C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。 - 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位
置,第一个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。
重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
例子:日期类实现
#pragma once
#include <iostream>
using namespace std;
class Date
{
public:
friend ostream& operator<<(ostream& out, Date& d);//友元函数,让非成员函数
//可以访问私有变量了
friend istream& operator>>(istream& in, Date& d);
Date(int year = 1, int month = 1, int day = 1);//显式构造函数
void Print() const;//const成员打印函数
int GetMonthDays(int year, int month)//定义在声明这儿默认内联
{
static int arr[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;
else return arr[month];
}
bool CheckDate()
{
return (_day > 0 && _day <= GetMonthDays(_year, _month)) && (_month > 0 && _month <= 12) && (_year > 0);
}
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
int operator-(const Date& d);//计算两个日期的相隔天数
Date& operator++();//前置++
Date operator++(int);//后置++,这个int形参只是为了形成重载,传个0或者1什么的都行
Date& operator--();
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
//流插入
ostream& operator<<(ostream& out, const Date& d);//ostream类的对象不支持拷贝,只能引用
//流提取
istream& operator>>(istream& in, Date& d);
//流插入和流提取只能定义在类外,因为成员函数第一个参数必是this指针
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print() const
{
cout << _year << '-' << _month << '-' << _day << endl;
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
bool Date::operator<=(const Date & d)
{
return !(*this > d);
}
bool Date::operator>(const Date& d)
{
if (_year > d._year) return true;
else if (_year == d._year && _month > d._month) return true;
else if (_month == d._month && _day > d._day) return true;
else return false;
}
bool Date::operator>=(const Date& d)
{
return(*this == d || *this > d);
}
bool Date::operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day) return true;
else return false;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
return *this -= (-day);
_day += day;
while (_day > GetMonthDays(_year, _month))
{
_day -= GetMonthDays(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);//拷贝构造
tmp += day;//复用一次+=
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
return *this += (-day);
_day -= day;
while (_day <= 0)
{
_month--;//先处理月份才能准确判断借来的天数
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDays(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);//与Date tmp = *this等价,都是拷贝构造
tmp -= day;
return tmp;
}
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
n++;
min++;
}
if (flag == 1) return n;
else return -n;
}
Date& Date::operator++()//前置++
{
*this += 1;
return *this;
}
Date Date::operator++(int)//后置++,这个int形参只是为了形成重载,传个0或者1什么的都行
{
Date tmp = *this;//Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
ostream& operator<<(ostream& out, Date& d)//ostream类的对象不支持拷贝,只能引用
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "主人~请依次输入年月日哦~ciallo~" << endl;
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "杂鱼主人~杂鱼~,日期非法哦" << endl;
}
else
{
break;
}
}
return in;
}
赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟
拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成
const 当前类类型引用,否则会传值传参会有拷贝,造成不必要的开销。 - 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋
值场景。 - 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷
贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义
类型成员变量会调用他的赋值重载函数。 - 所以,自己写不写赋值运算符重载和析构函数,拷贝重载函数逻辑相同。
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
强调一下赋值运算符重载和拷贝构造的区别
- 赋值运算符重载用于完成两个已经存在的对象直接的拷贝赋值。
- 拷贝构造用于⼀个对象拷贝初始化给另个要创建的对象
3. 取地址运算符重载
const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this。
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显式实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现⼀份,胡乱返回一个地址。
4.杂项
类型转换
• C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
• 构造函数前面加explicit就不再⽀持隐式类型转换。
• 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
class A
{
public:
// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
A(int a1)
:_a1(a1)
{}
//explicit A(int a1, int a2)//c++11之后才支持多参数类型转换
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{}
private:
int _b = 0;
static成员
- ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
- 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
- ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
- 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
- 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
- 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量
和静态成员函数。 - 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
- 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员
变量不属于某个对象,不走构造函数初始化列表。
友元
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类
声明的前面加friend,并且把友元声明放到一个类的里面。 - 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
- 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。
- 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
内部类(类的嵌套)
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在
全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。 - 内部类默认是外部类的友元类。
- 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考
虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其
他地方都用不了。
匿名对象
- 用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的
叫有名对象。 - 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
- const引用匿名对象可以延长匿名对象的生命周期。
对象拷贝时的编译器优化
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返
回值的过程中可以省略的拷贝。 - 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的编
译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行
跨行跨表达式的合并优化。