前言:
上一篇博客我们初步的介绍了类与对象,这篇让我们继续来学习类与对象吧!
个人主页:Pianeers
文章专栏:C++
如果有问题,欢迎评论区讨论!
希望能帮到大家,求点赞,关注加收藏!
目录
一、类中的默认成员函数
类的默认成员函数是指在创建一个类时,如果没有显式地定义函数,编译器会默认的生成一些函数,创建一个类默认会生成6个默认成员函数,其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后⾯再讲解。
默认成员函数很重要,我们可以从两个方面取学习:
①我们不写默认成员函数时,编译器默认生成的函数的行为是怎么样的?
②编译器默认生成的函数不满足我们的需求,我们需要自己实现,该如何实现以及需要注意什么。
二、构造函数
大家一看“构造”这两个字会误以为这个函数是用来开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),其实他的作用是对象实例化时用来初始化对象的。
如果我们不写构造函数,编译器自己生成的函数会怎样呢?
我们不写,编译器自己生成的构造函数对内置类型的成员变量初始化没有什么要求,是否会初始化取决于编译器自己;对于自定义类型的成员变量,会调用它的默认构造函数,如果它没有默认构造函数,就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们之后再细细讲解。
内置类型的成员变量初始化:
自定义类型的成员变量:
既然编译器自己写的构造函数不让人满意,那我们就自己写。我们自己写需要注意什么呢?
构造函数的特点:
①函数名与类名相同。
②无返回值。
③允许函数重载。
④对象一实例化,会自动调用构造函数。
⑤如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
这里还有一个非常容易弄混的知识点:默认构造函数。
很多人会以为这个就是编译器自己生成的函数,其实不是。默认构造函数包括:全缺省,无参以及我们不写编译器自己生成的。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。
class date
{
//
public:
//无参构造函数
/*date()
{
_year = 1;
_month = 1;
_data = 1;
}*/
//全缺省构造函数
/*date(int year = 10, int month = 20, int data = 90)
{
_year = year;
_month = month;
_data = data;
}*/
// 带参构造函数
date(int year, int month, int data)
{
_year = year;
_month = month;
_data = data;
}
void print()
{
cout << _year << '/' << _month << '/' << _data << endl;
}
private:
int _year;
int _month;
int _data;
};
int main()
{
date d1(2024,7,15);//带参构造函数
date d2();//无参构造函数或全缺省构造函数
d1.print();
}
三、析构函数
析构函数与构造函数功能基本相反,但析构函数并不是完成对对象本⾝的销毁 ,而是完成对象中资源的清理释放⼯作。⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管。如果类中没有资源,那我们就不需要写析构函数了。
析构函数的特点:
①析构函数名为类名前加~。(~+类名)
②析构函数无返回值。
③一个类中只有一个析构函数。
④对象生命周期快结束时,析构函数会自动调用。
⑤对于内置类型的成员变量,析构函数对它们不作用;对于自定义类型的成员变量,⾃定类型成员会 调⽤他的析构函数。
⑥⼀个局部域的多个对象,C++规定后定义的先析构。
⑦还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数。
我们什么时候需要写析构函数?
析构函数是完成资源清理的,如果此类中有从堆上申请的资源,使用new/malloc这些开辟出来的空间,就需要写析构函数。如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
cout << "Stack()" << endl;//如过调用了就打印出来
}
~Stack()
{
free(_a);
_capacity = _top = 0;//可写可不写
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
Stack st;//后定义的先被析构
return 0;
}
四、拷贝构造函数
拷贝构造函数是构造函数的一个重载函数,其作用是将一个对象复制到新创建的对象中。拷⻉构造函数的参数有⼀个且必须是类类型对象的引⽤。
拷贝构造函数的特点:
①拷⻉构造函数的第一个参数必须是类类型对象的引⽤,其它参数是默认值,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。
②C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
③若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成 员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构 造
什么情况下会调用拷贝构造函数?
①使用同类型的对象去初始化另一个对象时
int main()
{
date d1();
//下面两种都是拷贝构造函数
date d2(d1);
date d3 = d1;
}
②将一个对象作为实参传递给一个非引用类型的形参
void print(date d)
{
//.......
}
int main()
{
data d1();
printf(d1);
}
③从一个返冋类型为非引用类型的函数返回一个对象
传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没 有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少 拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
date ce(date d1)
{
return d1;
}
int main()
{
date d1();
ce(d1);
}
注意:将对象作为实参传递给拷贝构造函数时不能是传值传参。所以拷贝构造函数第一个参数是对类类型的引用。
默认的拷贝构造函数:
在未显示定义的类中,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数采用的是浅拷贝 (一个字节一个字节拷贝)。
若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
根据情况不同我们考虑的也不同:
①当类中只有内置类型且没有什么资源时,我们可以直接用默认的拷贝构造函数。
②当类中⾃定义类型成员变量会调⽤他的拷⻉构造。
③当类中虽然是内置类型但有资源时,我们需要写拷贝构造函数(深拷贝)。
对于③这种情况,如果我们用浅拷贝,对于里面的指针会拷贝两份且地址相同,当调用析构函数时会调用两次,一个地址销毁两次就会出错误。
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;
}
五、赋值运算符重载
5.1运算符重载
在之前的学习中我们知道运算符用于内置类型的比较,但是如今我们学了类,那么类该如何比较呢?
C++中不会直接给我们方法,但是C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错。
通过对运算符的重载,编译器就可以对类进行比较了。
运算符重载的格式:operator+运算符。
bool operator>( const date& d4)
{
return this->_year > d4._year
&& this->_month > d4._month
&& this->_day > d4._day;
}
运算符重载注意事项:
①和其他函数⼀样,运算符重载它也具有其返回类型和参数列表以及函数体 。
②重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。
③如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。
④运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
⑤不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
⑥.* :: sizeof ?: . 注意以上5个运算符不能重载。
我们还有一点需要注意:一般成员变量都会被限制,当我们把运算符重载定义在全局时,成员变量我们无法调用,这里有最好的解决方法是:将运算符重载定义在类内。
class date
{
public:
date(int year = 2024, int month = 7, int day = 16)//构造函数
{
_year = year;
_month = month;
_day = day;
}
date(const date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
bool operator>( const date& d4)
{
return this->_year > d4._year
&& this->_month > d4._month
&& this->_day > d4._day;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void pint(date d1)
{
cout << 5 << endl;
}
int main()
{
date d1(1,2,3);
date d2(4, 5, 6);
d1.operator>(d2);
}
5.2赋值运算符重载
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
赋值运算符重载的特点:
①赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。
②当类内的成员变量都是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就 可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是 内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我 们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。
③有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋 值场景
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引⽤返回减少拷⻉
// d1 = d2;
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 5);
Date d2(d1);
Date d3(2024, 7, 6);
d1 = d3;
// 需要注意这⾥是拷⻉构造,不是赋值重载
// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;
return 0;
}
六、日期类的实现
//date.h
#pragma once
#include<iostream>
using namespace std;
class date
{
friend ostream& operator<<(ostream& out, const date& d);
friend istream& operator>>(istream& in, date& d);
public:
//初始化
//缺省函数声明和定义分离时,缺省值在声明中
date(int year = 2024, int month = 7, int day = 16);
void print();
//检查
bool CheckDate(int month,int day);
//取一个月的天数
int Getday(int year, int month)
{
static int monthday[13] = { -1, 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 monthday[month];
}
//d1+=day
//类中的数据发生修改
date& operator+=(int day);
//da+day;
//类中的数据未发生修改
date operator+(int day);
//d1-=day
date& operator-=(int day);
//d1-day
date operator-(int day);
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& d0);
//后置++
date operator++(int i);
//前置++
date operator++();
//d1-d2
int operator-(const date& d0);
//<<
private:
int _year;
int _month;
int _day;
};
//date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"date.h"
//初始化
date::date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate(month,day))
{
cout << "非法日期:";
print();
}
}
//打印日期
void date::print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
date& date::operator+=(int day)
{
_day += day;
while (_day>Getday(_year,_month))
{
_day = _day - Getday(_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).operator+=(-day);
}
_day -= day;
while(_day<=0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year;
}
_day += Getday(_year, _month);
}
return *this;
}
date date::operator-(int day)
{
date tmp = *this;
tmp -= day;
return tmp;
}
bool date::operator>=(const date& d)
{
if (!(*this < d))
{
return true;
}
return false;
}
bool date::operator>(const date& d)
{
if (*this <= d)
{
return false;
}
else {
return true;
}
}
bool date::operator<=(const date& d)
{
return *this < d || *this == d;
}
bool date::operator<(const date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
bool date::operator==(const date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool date::operator!=(const date& d0)
{
if (!(*this == d0))
{
return true;
}
return false;
}
date date::operator++(int i)
{
date tmp = *this;
tmp += 1;
return tmp;
}
date date::operator++()
{
*this += 1;
return *this;
}
int date::operator-(const date& d0)
{
int n = 0;
int flag = 1;
date max(*this);
date min(d0);
if (max < min)
{
max = d0;
min = *this;
flag = -1;
}
while (max != min)
{
++min;
++n;
}
return n*flag;
}
bool date::CheckDate(int month, int day)
{
if (month > 12 || month < 1 || day > Getday(_year, _month) || day < 1)
{
return false;
}
return true;
}
ostream& operator<<(ostream& out, const date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, date& d)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
while (1)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate(d._month,d._day))
{
cout << "输入日期非法:";
d.print();
cout << "请重新输入!!!" << endl;
}
else
{
break;
}
}
return in;
}
七、取地址运算符重载
7.1const成员函数
const成员函数特点:
①将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
②const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
#include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // void Print(const Date* const this) const void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩ Date d1(2024, 7, 5); d1.Print(); const Date d2(2024, 8, 5); d2.Print(); return 0; }
7.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当 前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址.
class Date
{
public :
Date* operator&()
{
return this;
// return nullptr;
const Date* operator&()const
{
return this;
// return nullptr;
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};