文章目录
- 类的6个默认成员函数
- 初始化和清理
- 构造函数
- 构造函数概念
- 构造函数特征
- 析构函数
- 析构函数概念
- 析构函数特征
- 拷贝赋值
- 拷贝构造函数
- 拷贝构造函数概念
- 拷贝构造函数特征
- 赋值运算重载
- 运算符重载
- 运算符重载特征
- 赋值运算符重载
- 赋值运算符特征
- 取地址重载
- 取地址操作符重载
- const取地址操作符重载
- const成员函数
大纲
类的6个默认成员函数
如果一个类中什么成员都没有 称之为空类
class Empty
{
};
空类并不是什么都没有 编译器会自动生成6个默认成员函数
默认成员函数: 用户自己没有写 编译器会自动生成 若用户自己写了 则不会自动生成
初始化和清理
构造函数
构造函数概念
构造函数是一种特殊的成员函数 名字和类的名字一样 创建类类型时由编译器自动调用 用来完成对象资源的初始化工作
在整个生命周期内只调用一次
比如下面这个Date类
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//打印结果为0-0-0
return 0;
}
Date就是成员函数就是一个构造函数 创建一个Date对象实例化时 编译器会自动调用这个构造函数对成员变量进行初始化
构造函数特征
1.函数名与类名相同
2. 无返回值
无返回值不是void 是不用指定函数类型
3. 对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
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;//调用无参构造函数
d1.Print();
Date d2(2023, 10, 21);//调用有参构造函数
d2.Print();
return 0;
}
构造函数也支持缺省值 上面的代码可以改写成这样
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_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(2023, 10, 21);
d2.Print();
return 0;
}
5.如果类中用户没有定义构造函数 则编译器会自动生成一个无参的默认构造函数 一但用户定义 编译器便不在生成
//编译器自动生成构造函数
class Date
{
public:
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用编译器生成的构造函数初始化
d1.Print();//打印结果为随机值
return 0;
}
初始化结果为随机值??? 这里的构造函数貌似没什么用
编译器自动生成构造函数的规则:
对内置类型(int/char/指针类型等)不做处理
对自定义类型(class/struct/union等)会自动调用默认构造函数
c++11中针对内置类型成员不初始化的缺陷 打了新补丁 即内置成员变量在类中声明可以给默认值
仅仅只是声明 并没有定义 没有开空间
class Date
{
public:
void Print()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
int _year = 0;
int _month =0;
int _day =0;
};
int main()
{
Date d1;//调用编译器生成的构造函数初始化
d1.Print();//打印结果为0-0-0
return 0;
}
6.无参构造函数和全缺省构造函数和编译器默认生成的构造函数都称为默认构造函数
默认构造函数有且只能有一个
错误示例
: 无参和全缺省同时出现
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 0, int month = 0, int day = 0)
{
_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();
return 0;
}
结合构造函数的特征 :一般情况下都需要自己写构造函数 成员时自定义类型 或者声明给了缺省值 可以考虑让编译器生成默认构造函数
析构函数
析构函数概念
与构造函数的功能相反 析构函数不是完成对象本身的销毁工作 局部对象销毁工作是由编译器完成的 而对象在销毁时会自动调用析构函数 完成对象中的资源清理工作
析构函数特征
1、析构函数的函数名 == ~类名
class Date
{
public:
Date()//构造函数
{
}
~Date()//析构函数
{
}
};
2、析构函数和构造函数一样无返回值 还有一点是无参函数
3、一个类只能有一个析构函数 如果未定义 编译器会自动生成默认的析构函数
默认析构函数规则和构造函数一样
4、析构函数不能重载结合2 3特性
5、对象生命周期结束时 c++编译器自动调用析构函数
class Date
{
public:
~Date()
{
cout << "111" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//会打印111 编译器自动调用析构函数
return 0;
}
结合析构函数的特征 一般如果类中没有资源申请时 析构函数可以不写 直接使用编译器默认生成的即可;
如果有资源申请时 一定要写 否则会造成内存泄漏
拷贝赋值
拷贝构造函数
拷贝构造函数概念
只有单个形参 该形参是对本类类型对象的引用一般用const修饰,在用已存在的类类型对象创建新对象时由编译器自动调用
class Date
{
public:
Date(int year = 1, int month = 1 , int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造函数 d是d1的别名 ,this指向d2
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year<< "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,10,27);
d1.Print();
Date d2(d1);//用d1去初始化d2
d2.Print();
return 0;
}
拷贝构造函数特征
1、是构造函数的一个重载形式
拷贝构造函数名也和类名相同 参数一般和构造函数不同
2、参数只能有一个且必须是类类型对象的引用
Date(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
上面的Date类拷贝构造函数写成这样的话会引发无穷递归 聪明的编译器会直接报错
3、若未定义 则编译器会默认生成拷贝构造函数
class Date
{
public:
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(d1);//用d1去初始化d2
d2.Print();
return 0;
}
默认生成拷贝构造函数的规则:
下面举个例子
class Time
{
public:
Time(int _hour = 1, int _minute = 1, int _second = 1)//构造函数
{}
Time(const Time& t)//拷贝构造函数
{
cout << "Time(const Time& t)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2(d1);//用d1拷贝构造d2 此处会调用Date类的拷贝构造函数
//但是Date的拷贝构造函数是编译器默认生成的 自定义变量_t就会去调用Time类的拷贝构造
//所以这里会打印 Time(const Time& t)
return 0;
}
4、编译器自动生成的不能完成深拷贝
错误示范
class Stack
{
public:
Stack(int capacity = 10)//构造函数
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
~Stack()//析构函数
{
free(_arr);
_arr = nullptr;
_size = _capacity = 0;
}
void Push(int x)
{
_arr[_size] = x;
_size++;
}
private:
int* _arr;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
Stack s2(s1);//
return 0;
}
上面栈这个例子s1对象调用构造函数初始化 然后存放了2个数据1和2
s2对象使用s1的拷贝构造,Stack类中没有自己实现这个函数 所以编译器会默认生成一个拷贝构造函数 默认的拷贝构造函数是按照值拷贝的 将s1中内容原封不动拷贝到s2中 因此s2和s1指向了同一块内存空间
当程序退出时 s1和s2 要销毁 s2销毁时回去调用析构函数 以及释放了0x0000017f49e6d510这块空间 ,s1再销毁 也会去销毁这块空间 ,一块空间同时多次释放 会引起程序奔溃
实际的栈中 s2和s1应该是不同空间的两个对象这个代码显然是不能完成的
结合拷贝构造函数的特征 如果类中没有设计到资源申请时,拷贝构造函数写不写都可以,编译器默认生成可以完成了;如果有涉及到资源申请时 则拷贝构造函数时一定要自己实现的,否则就是浅拷贝
上面例子的拷贝构造函数的正确实现方式
Stack(const Stack& stt)// 拷贝构造函数
{
// 深拷贝
_arr = (int*)malloc(sizeof(int) * stt._capacity);
if (_arr == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_arr, stt._arr, sizeof(int) * stt._size);
_size = stt._size;
_capacity = stt._capacity;
}
赋值运算重载
运算符重载
内置类型运算符一般有算术运算符 关系运算符 比较运算符 赋值运算符
等等
如何实现自定义类型的运算 比如判断两个日期是否相等
可以用一个函数进行判断 例如
bool IsSame(Date& x,Date& y)
{
if (x._year == y._year && x._month == y._month && x._day == y._day)
return true;
else
return false;
}
这样写最大的缺点是可读性太差 C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
可以重定义或重载大部分 C++ 内置的运算符。这样,就能使用自定义类型的运算符
运算符重载特征
1、运算符重载函数格式
函数原型:返回值类型 operator操作符(参数列表)
2、重载运算符必须有一个类类型参数
bool operator==(int, int );//错误示范
bool operator==(int, Date );//正确
3、对内置类型运算符含义不能改变
4、作为类成员函数重载时 函数有一个默认的形参this
错误示范
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& x, const Date& y)//运算符重载函数
{
return x._year == d._year
&& x._month == d._month
&& x._day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023,10,28);
return 0;
}
作为类成员函数时 这样的写法是错误的 因为还有一个this ==的操作数只有两个 参数有三个了 编译器会报错
正确实现运算重载函数
bool operator==(const Date& y)
{
return _year == y._year
&& _month == y._month
&& _day == y._day;
}
可以看成下面这种写法
bool operator==(const Date& y)
{
return this->_year == y._year
&& this->_month == y._month
&& this->_day == y._day;
}
5、编译器不会默认生成 需要自己实现
运算符包含关系运算符 算数运算符 逻辑运算符 赋值运算符 等等 只有赋值运算符函数编译器才会默认生成
6、.* :: sizoef ?: .
不支持重载
赋值运算符重载
赋值元素符和其他运算符不同 他会更新变量的值
内置类型的赋值运算符一般有 = += -= /= *=
…等等
赋值运算符特征
1、赋值运算符重载的格式
- 参数类型 :const 类名& 传引用可以提高传参效率
- 返回值类型: 类名& 返回引用可以提高返回的效率 有返回值目的是为了支持连续赋值
- 返回*this 要符合连续赋值的含义
- 检查是否自己给自己赋值
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(1,1,1);
Date d2(2,2,2);
d1 = d2;//将d2赋值给d1
d1.Print();//打印2-2-2
d2.Print();//打印2-2-2
}
2、赋值运算符只能重载成类的成员函数 不能重载成全局函数
错误示范
class Date
{
public:
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;
};
Date& operator=(const Date& d)//赋值运算符重载函数
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
赋值运算符如果不写 编译器会默认生成一个 我们再在类外面自己实现一个全局的 就会和默认的发生冲突 所以赋值运算符重载只能是类的成员函数
3、用户未定义 编译器默认生成一个
编译器默认生成的对内置类型完成值拷贝 对自定义类型会去调用他自己的赋值运算符重载函数
class Time
{
public:
Time(int hour = 1,int minute = 1,int second = 1)
{
_hour = hour;
_minute = minute;
_second = second;
}
Time& operator=(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time& operator=(const Time& t)" << endl;
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 1;
int _month =1 ;
int _day =1;
Time _t;//自定义类型t
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
d2赋值给d1 Date成员中内置类型会去调用默认生成的赋值运算符重载函数 自定义类型t会去调用自己的赋值运算符重载函数 所以程序会打印出Time& operator=(const Time& t)
默认生成的赋值运算符函数可以完成字节序的值拷贝 有些场景还需要自己实现的 和拷贝构造函数一样
取地址重载
取地址操作符重载
class Date
{
public:
Date* operator&()//取地址操作符重载函数
{
cout << "Date* operator&()" << endl;
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cout << &d1 << endl;
return 0;
}
上面例子会打印Date* operator&() 以及d1的地址
如果上面的取地址操作符重载函数写成这样
Date* operator&()
{
return NULL;
}
返回的就是0地址 这样写一般也没有应用场景 取地址操作符重载一般用编译器自己默认实现的即可
const取地址操作符重载
在介绍const取地址操作符重载函数之前先介绍一下const成员函数
const成员函数
我们将用const修饰的成员函数称之为const成员函数
const修饰类的成员函数实际上是修饰该成员函数隐含的this指针,表明在该成员函数中不对类的任何成员进行修改
下面这个例子
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
void Print() const//const成员函数
{
cout << "Print() const" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,11,02);
d1.Print();//打印Print()
const Date d2(2023,11,02);
d2.Print();//打印Print() const
return 0;
}
非const对象d1 会去调用非const成员函数
const对象d2调用const成员函数
改写成下面这种写法
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const//const成员函数
{
cout << "Print() const" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,11,02);
d1.Print();//打印Print()const
const Date d2(2023,11,02);
d2.Print();//打印Print() const
return 0;
}
非const对象d1 和const对象d2 都会区调用const成员函数
改写下面这种写法
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,11,02);
d1.Print();//打印Print()const
const Date d2(2023,11,02);
d2.Print();//error
return 0;
}
非const对象d1 会去调用非const成员函数
但是用const对象d2调用非const成员函数 会报错
原因是因为成员函数隐含的this指针的类型是Date* const this 而d2是const Date类型 类型不匹配
总和上面的例子
const对象只能调用const成员函数
非cosnt对象既能调用cong成员函数也可以调用非const成员函数
如果某些成员函数需要修改成员变量 那么则不能使用const修饰
能用const修饰的成员函数 一般都应该定义成cosnt
下面再来看const取地址操作符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
cout << "Date* operator&() " << endl;
return this;
}
const Date* operator&() const
{
cout << "const Date* operator&() const " << endl;
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
cout << &d1 << endl;//打印Date* operator&() 然后在打印d1的地址
const Date d2;
cout << &d2 << endl;//打印const Date* operator&() const 然后打印d2的地址
return 0;
}
编译器会去调用和自己匹配的成员函数
一般这两个成员函数直接使用编译器默认生成的即可 不用自己实现