下面是关于CPP“类和对象(中)”的一些知识点分享,主要是围绕CPP中的六大默认成员函数来说的,有需要借鉴即可。
点个赞再看,你最好看~
类和对象是CPP基础的一个难点,并不在于思维逻辑的难度,而是交织了许多细节性语法和知识,因而今天特地对类和对象中的默认成员函数做了整理,希望可以帮助读者理清其中的细节语法,并建立系统化思维。
首先,我们应该对默认成员函数有所大体了解:
1.初识六大默认成员函数
1.1概念
在自己不写的情况下,编译器会自动默认生成的六个默认成员函数。
分别包含以下六个:
- 1.构造函数:初始化
- 2.析构函数:清理数据
- 3.拷贝构造:类对象生成时的数值拷贝
- 4.复制重载:已存在的类对象的数值拷贝
- 5.取地址重载:返回类对象的地址
- 6.const取地址重载:返回类对象的地址,仅可读
上面仅仅是对默认函数的简单概括,下面来具体介绍每个不同的函数是怎么用的及其相关细节~~
2.构造函数
CPP规定,每个类进行实例化时必须进行初始化,而这个“初始化”工作是由构造函数所完成的。
2.1概念
完成类对象初始化工作的特殊成员函数。
而这个初始化,指的是类对象与生俱来的初始化,如同婴儿刚生下来的五行属性:
那这个特殊的成员函数特殊在哪里呢?下面来介绍构造函数的特性。
2.2特性
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的默认构造函数
- 构造函数可以重载
思考:如果忘记定义构造函数怎么办?
答:编译器会默认自动生成无参数构造函数
注:但是如果你写了构造函数,编译器就不会再生成无参构造函数了
编译器自动生成的无参数构造函数与默认构造函数的概念区分:
答:编译器生成的无参数构造函数包含于编译器自动调用的默认构造函数之中。
编译器自动生成的默认构造函数介绍:
3.析构函数
3.1概念
完成类对象资源清理工作的特殊成员函数。
析构函数就像一个清理工,对类中的堆开辟的内容释放掉,对一些资源释放出去…
那这个析构函数有什么特性呢?下面来进行介绍:
3.2特性
- 析构函数名是在类名前加~号
- 无参数、无返回类型
- 一个类中只能有一个析构函数,若未显示定义,系统会默认生成一个
- 对象生命周期结束时,会自动调用析构函数。
思考:析构函数忘记写怎么办?
思考:多个类对象,销毁顺序是如何的呢?
答:这与生命周期相一致,先销毁局部变量,再销毁局部静态变量,最后销毁全局变量(无论静态非静态),同是(静态)局部变量/全局变量则遵循先创建后销毁的规律。
4.拷贝构造函数
嗯…听这个名字,拷贝构造函数这个与构造函数什么关系呢?对,拷贝构造函数属于构造函数的一种。
4.1概念
CPP规定,自定义类型的拷贝必须要用拷贝构造函数来完成。
用于正在创建的类对象,拷贝另一个已存在的类对象的特殊成员构造函数。
这个拷贝构造函数有点像我们下载qq,我们电脑里是没有qq的,所以要下载一个文件,这个文件的内容是什么呢?哎,我们把qq官网上的qq文件给拷贝过来。
思考:拷贝构造函数的参数为什么是引用?
- 1.如果是传值传参,会出现无限递归
- 2.如果用指针,系统会识别为构造函数重载,而不作为拷贝构造函数来使用
因为当我们用指针实现一个所谓的“拷贝构造函数时”,编译器会调用默认生成的,也就是说编译器会把我们自己写的指针版的拷贝构造函数识别成构造函数重载。
//拷贝构造为什么不能用指针?因为用指针写的拷贝构造函数会被编译器识别为构造函数重载 #include<iostream> using namespace std; class Date { private: int _year; int _month; int _day; public: //构造函数 Date() { } //拷贝构造函数?构造函数重载? Date(Date* d) { cout << "Date(Date* d)" << endl; this->_year = d->_year; this->_month = d->_month; this->_day = d->_day; } }; int main() { Date d1; Date d2(d1); return 0; }
4.2特性
- 1.自己不写,编译器会默认生成
编译器默认生成的拷贝构造函数我们称之为默认拷贝构造函数
该函数对内置类型进行值拷贝(浅拷贝),对于自定义类型会去调用他的拷贝构造函数思考:我们是否可以在任何场景下都不写拷贝构造函数?
答:不是。这是因为编译器的默认拷贝构造函数是进行的值拷贝,一旦出现指针,需要进行深拷贝的时候就会出问题。
比如,有一个栈,栈A指向一块空间,如果把栈A拷贝给栈B,那么B也指向那块空间,到时候free的时候,free两次会报错。
- 2.深拷贝与浅拷贝的对比
总结来说,需要用到动态开辟资源的情况,需要进行深拷贝;单纯值拷贝,编译器默认生成的即可完成相关工作。
//栈的深拷贝与浅拷贝,浅拷贝的反例。 #include<iostream> using namespace std; class Stack { private: int* arr; int top; int capacity; public: //构造函数 Stack(int n = 10) { int* t_arr = (int*)malloc(sizeof(int) * n); if (t_arr == nullptr) { perror("malloc fail"); return; } this->arr = t_arr; this->capacity = 10; this->top = 0; } //析构函数 ~Stack() { free(this->arr); } //拷贝构造函数 //1.值拷贝error /*Stack(Stack& st) { this->arr = st.arr; this->capacity = st.capacity; this->top = st.top; }*/ //2.深拷贝right Stack(Stack& st) { int* t_arr = (int*)malloc(sizeof(int) * 10); if (t_arr == nullptr) { perror("malloc fail"); return; } this->arr = t_arr; this->capacity = st.capacity; this->top = st.top; } }; void test1() { Stack s1; Stack s2(s1); }
- 3.拷贝构造的调用逻辑
那什么时候调用我们自己写的拷贝构造函数,有什么时候调用编译器自己生成的呢?
如下:
#include<iostream> using namespace std; //自己写的构造函数在不主动调用情况下,会自动调用自定义的默认构造吗?会。 class Time { public: Time() { cout << "Time" << endl; } int _hour; int _minte; int _second; }; class Date { int _year; int _month; int _day; Time _time; public: Date() { cout << "date" << endl; } }; int main() { Date d1; return 0; }
#include<iostream> using namespace std; //拷贝构造函数的调用:Date中的拷贝构造显式定义要明确写调用Time中的拷贝构造编译器才回去调用,不然编译器会自动调用默认的拷贝构造 class Time { public: int second = 20; int minte = 20; int hour = 20; public: Time() { } Time(Time& t) { this->hour = t.hour; this->minte = t.minte; this->second = t.second; } }; class Date { int _year; int _month; int _day; Time _time; public: Date() { this->_day = 10; this->_month = 10; this->_year = 10; } Date(Date& d) { this->_day = d._day; this->_month = d._month; this->_year = d._year; Time(d._time); } void Print() { cout << _day << _month << _day <<"/" << _time.hour << _time.minte << _time.second << endl; } }; int main() { Date d1; Date d2(d1); d1.Print(); d2.Print(); return 0; }
思考:为什么建议拷贝构造函数参数前面加上const来修饰引用?
答:为了防止拿来拷贝的样本被反修改了。
5.赋值运算符重载
现在有下面这样一个场景,要求写一个日期类判断两个日期是否相等的函数
于是,只会C的你,是这样写的:
5.1概念
允许自定义类型进行运算的特殊成员函数
语法:
返回值类型 operator操作符(参数列表)
思考:为什么定义在类内的函数少了一个参数?
答:实际上是因为默认类内的函数都有一个this指针,所以这里把Date& d1换成了隐含的this指针而已。
5.2特性
- 1.不能通过operator重载语言中没有的操作符,比如¥;还有5个不能重载的运算符,包括 点✳ 域指定符 sizeof 三目操作符 点,共计5个
- 2.重载操作必须有一个类类型
- 3.建议:用于内置类型的操作符,重载后含义不变
- 4.当重载函数定义在类内时,其形参一个被this代替
- 5.如果自己不写且用到了自定义类型的赋值,编译器会自动生成一个,对默认类型进行赋值,对自定义函数回去调用自定义类型的赋值函数。(赋值运算符必须是成员函数)
5.3赋值运算符的实现:
①
②我们知道,对于内置类型,等号提供连续赋值,所以我们实现的赋值函数重载也需要实现连续赋值:
思考:编译器提示在连续使用赋值函数时候有报错,为什么?
答:因为值拷贝会去调用拷贝构造函数,值拷贝生成临时变量去返回,而临时变量具有常性,即const只可读,所以赋值运算符重载函数的参数也应该要加上const,const Date& d
③
④我如果返回类型用引用可以吗?
答:可以,而且相比值拷贝更好一些,因为没有了中间变量,效率会提升一些。
拓:如果返回类型和参数都改用指针呢?
答:不行,连续赋值怎么接收?
⑤
思考:为什么这个地方要加上if判断?
答:防止有人无聊自赋值,浪费性能,这样可以在有人自赋值的情况下提高效率。
思考:拷贝构造函数与赋值运算符重载的区别在哪里?
答:
拷贝构造:同类型的已存在对象去初始化正在创建的一个对象
赋值运算符函数重载:是一个已经存在的对象,拷贝赋值给另一个已经存在的对象。
6.Date日期类实现
#pragma once #include<iostream> #include<assert.h> using namespace std; class Date { private: //友元函数 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); int _day; int _month; int _year; public: Date(int year = 1, int month = 1, int day = 1); void DatePrint() const; 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+=(const int day); Date operator+(const int day); Date& operator++();//前置++ Date operator++(int);//后置++ int operator-(const Date& d);//算两个日期相差多少天 bool CheckInvalid(); //bool operator=(const Date& d); inline bool leap_year(int& year)//频繁调用,类内函数自动为inline { if (((year % 4 == 0) && year % 100 != 0) || year % 400 == 0) { return true; } else { return false; } } inline int GetMonthDay(int year, int month)//频繁调用,类内函数自动为inline(可以不用加inline) { assert(month >= 1 && month <= 12); static int months[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//细节:改静态 if (month == 2 && leap_year(year))//细节:左右一换 return months[month] + 1; return months[month]; } /*Date(Date& d) { cout << "拷贝构造" << endl; }*/ }; //实现在类外作为全局函数 ostream& operator<<(ostream& out, const Date& d); istream& operator>>(istream& in, Date& d);
#include"Date.h" Date::Date(int year, int month, int day) { this->_day = day; this->_month = month; this->_year = year; if (!CheckInvalid()) { cout << "error,reprint" << endl; } } void Date::DatePrint() const { cout << "year:" << this->_year << " "; cout << "month:" << this->_month << " "; cout << "day:" << this->_day << endl; } bool Date::operator<(const Date& d) { if (this->_year < d._year) { return true; } else if(this->_year == d._year) { if (this->_month < d._month) { return true; } else if (this->_month == d._month) { if (this->_day == d._day) { return true; } } } return false; } bool Date::operator<=(const Date& d) { return (*this == d) || (*this < d); } bool Date::operator>(const Date& d) { return !(*this <= d); } bool Date::operator>=(const Date& d) { return !(*this < d); } bool Date::operator==(const Date& d) { return this->_year == d._year && this->_month == d._month && this->_day == d._day; } bool Date::operator!=(const Date& d) { return !(*this == d); } Date& Date::operator+=(const int day) { this->_day += day; //进位 while (this->_day > GetMonthDay(this->_year, this->_month)) { this->_day -= GetMonthDay(this->_year, this->_month); this->_month++; if (this->_month == 13) { _year++; _month = 1; } } return *this; } Date Date::operator+(const int day)//思考:这个地方为什么不用引用返回?答:因为我们返回的是一个栈内的临时Date,出函数会销毁。 { //Date temp(*this); Date temp = *this;//思考:这里是拷贝构造还是赋值?答:拷贝构造。为什么?因为拷贝构造与对象的诞生一同进行,而赋值是对于已经存在的对象而言的。 temp._day += day; //进位 while (temp._day > GetMonthDay(temp._year, temp._month)) { temp._month++; temp._day -= GetMonthDay(temp._year, temp._month - 1); if (temp._month == 13) { _year++; _month = 1; } } return temp; } +复用+= //Date Date::operator+(const int day) //{ // Date temp = *this; // temp += day; // // return temp; //} +=复用+ //Date& Date::operator+=(const int day) //{ // *this = (*this + day); // // return *this; //} Date& Date::operator++()//前置++ { *this += 1; return *this; } Date Date::operator++(int)//后置++ { Date temp = *this; *this += 1; return temp; } int Date::operator-(const Date& d) { Date max = *this; Date min = d; if (*this < d) { max = d; min = *this; } int sub = 0; while (min != max) { ++min; sub++; } return sub; } ostream& operator<<(ostream& out, const Date& d) { out << d._year<<"年" << d._month << "月" << d._day << "日" << endl; return out; } istream& operator>>(istream& in, Date& d) { while (true) { in >> d._year >> d._month >> d._day; if (!d.CheckInvalid()) { cout << "error, refail" << endl; continue; } else { break; } } return in; } bool Date::CheckInvalid() { if (_year <= 0 || _month < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year, _month)) { return false; } else { return true; } }
#include"Date.h" void test1() { /*Date d1(9,1,9); Date d2(6,8,3); Date d3(10,8,9); cout << (d1 <= d2) << endl; cout << (d1 < d3) << endl; Date d4(2024, 4, 20);*/ /*Date d5(2023,2,29); d5 += 20; d5.DatePrint();*/ /*Date d6(1, 2, 3); Date d7(d6+=10); d7.DatePrint(); d6.DatePrint();*/ /*Date d8(2024, 4, 20); Date d9(2023, 3, 31); int sub = d8 - d9; cout << sub << endl;*/ } void test2() { Date d1; Date d2(2, 2, 2); //cout << d1; //写法1: //d1 << cout;//奇怪的写法 //写法2: //d1.operator<<(cout); cin >> d1 >> d2; cout << d1 << d2; } void test3() { /*Date d1(2024, 4, 21); d1.DatePrint();*/ const int a = 10; //1.拷贝 int b = a; //2.权限放大 int& c = a; } int main() { //const 成员函数的认识 test3(); //test1(); //test2(); }
7.const成员变量/函数
7.1概念
const修饰this指针的指向内容,使this不能改变其指向内容的成员变量/函数
7.2理解
如何给一个”看不到“的this加上const呢?
CPP提供了以下语法:
拓展:权限处理的三种情况(同一个变量):
思考:权限与赋值拷贝的区分?
8.取地址、const取地址操作符重载
8.1概念
用于返回对象地址的特殊重载取地址成员函数,不写编译器会自动生成。
注:编译器会自动生成两个,一个使带const的取地址操作符重载,另一个使不带const的取地址操作符重载
8.2语法
- 1.正常取类对象的地址:
- 2.可以返回空/假地址,避免别人找到类对象的地址