本章重点
- 上篇
- 1.面向过程和面向对象初步认识
- 2.类的引入---结构体
- 3.类的定义
- 3.1 语法
- 3.2 组成
- 3.3 定义类的两种方法:
- 4.类的访问限定符及封装
- 4.1 访问限定符
- 4.2封装---面向对象的三大特性之一
- 5.类的作用域
- 6.类的实例化
- 7.类对象模型
- 7.1 如何计算类对象的大小
- 8.this指针
- 8.1 this指针基本认识
- 8.2 this 指针的特点
- 8.3 代码解析
- 中篇
- 1.类的6个默认成员函数
- 2.构造函数
- 3.析构函数(需要传参)
- 3.1 概念
- 3.2 性质
- 3.3 使用情况
- 4.拷贝构造函数
- 4.1 概念
- 4.2 特性
- 5.赋值运算符重载
- 5.1 运算符重载
- 5.2 赋值运算符重载
- 5.3 赋值运算符重载的特性
- 6.const成员函数
- 7.取地址及const取地址操作符重载
- 下篇
- 1.再谈构造函数
- 1.1 函数体内赋值
- 1.2 初始化列表(构造函数的一部分)
- 1.3 explicit关键字
- 2.static成员
- 2.1 概念
- 2.2 特性
- 3.友元
- 3.1 友元函数
- 3.2 友元类
- 4.内部类
- 5.匿名对象
- 6.拷贝对象时的一些编译器优化
- 练习
- 1.Date代码
- Date.h
- Date.cpp
- 2.OJ题
- 2.1 [求1+2+3+...+n(不用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
- 2.2 [计算一年的第几天](https://www.nowcoder.com/practice/769d45d455fe40b385ba32f97e7bcded?tpId=37&&tqId=21296&rp=1&ru=/activity/oj&qru=/ta/huawei/question-ranking)
- 2.3 [日期差值](https://www.nowcoder.com/practice/ccb7383c76fc48d2bbc27a2a6319631c?tpId=62&&tqId=29468&rp=1&ru=)与[打印日期](https://www.nowcoder.com/practice/b1f7a77416194fd3abd63737cdfcf82b?tpId=69&&tqId=29669&rp=1&ru=/activity/oj&qru=/ta/hust-kaoyan/question-ranking)
上篇
1.面向过程和面向对象初步认识
- C语言,是面向过程的,关注的是过程,分析出求解问题的每一步,通过函数调用来逐步解决
- C++,是面向对象的,关注的是对象,讲一件事拆分为不同的对象,靠对象之间的交互完成
我们用一个具体的事件来理解,就比如洗衣服这件事
- 用面向过程的方法:(C语言)
- 打开洗衣机
- 放入衣服和洗衣液
- 洗衣机开始洗衣服
- 等待洗完,拿出衣服晾干
- 用面向对象的方法:(C++)
这件事分为两个对象:人 和 洗衣机,在对每一个对象进行分析,它需要做哪些事情
- 人做三件事:
- 打开洗衣机
- 放入衣服和洗衣液
- 拿出衣服晾干
- 洗衣机只需要做一件事:
- 洗衣服即可
总结:
我们能看出来,面向过程和面向对象是两种思考问题的方式,处理问题的角度不一样
面对过程的思维方式,更加关注事情的每一步。能更加高效、直接,
面对对象的思维方式,更加关注事情的参与者、分类。对每一个类有不同的规划,这样在这件事的解决方面能更好的管理和维护
2.类的引入—结构体
- 类定义:类 +类名+{ };
- 在C++中,结构体被升级成了类,但C语言的用法还是可以使用的.
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数。
struct Student //class Struct 是C++中常用的使用方法
{void Init(const char* name, const char* gender, int age){strcpy(_name, name);strcpy(_gender, gender);_age = age;}void Print(){cout << _name << " " << _gender << " " << _age << endl;}// 这里变量的声明并不是必须加_// 习惯加这个,用来标识成员变量char _name[20];char _gender[3];int _age;
};int main()
{struct Student s1; // C语言的用法,需要加上structStudent s2; // C++中,可以直接用Students1.Init("夜猫子", "O.o", 123);s1.Print();return 0;
}
在结构体的定义中,C++更习惯于用 class 来替代 struct
3.类的定义
- 与结构体相似:class + 类名 + {} + ;
- 注意:;——分号不可省
3.1 语法
class classname
{//类体:由成员变量和成员函数组成//...
}
- class 是定义类的关键字
- classname 是类的名称
- { }; 中的内容是主体,注意结束时的分号
3.2 组成
- 类中的元素:称为类的成员
- 类中的变量:称为成员变量
- 类中的函数:称为成员函数
3.3 定义类的两种方法:
- 在类里面定义函数
需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Data
{//类里的内容由成员函数和成员变量组成
public:Data(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year = 0;int _month = 0;int _day = 0;
};
- 在类外定义函数——类域
void student::Print()
{cout << "Print" << endl;
}
class student
{void Print();char _name[6];int _age;int _number;student* _p;
};
注意: 在类外定义函数,需要在类里面声明一下这个函数,并且要在定义的时候函数名前加上类名+作用域限定符,表明这是该类的函数。
且我们常常将声明放在
.h
文件里面,类的定义放在.cpp
文件里面(长的分离,短的在内部)
4.类的访问限定符及封装
4.1 访问限定符
C++的封装实现方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
# 访问限定符
## public(公有)
## protected(保护)
## private(私有)
访问限定符的说明:
public
修饰的成员在类外可以直接被访问protected
和private
修饰的成员在类外不能直接被访问
(此处protected和private是类似的,他们的不用以后会讲)- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到
}
即类结束。
5.** class的默认访问权限**为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++ 需要兼容 C 语言,所以 C++ 中 struct 可以当成结构体去使用。
另外 C++ 中 struct 还可以用来定义类,和 class 是定义类是一样的;
区别是 struct 的成员默认访问方式是 public,class 是的成员默认访问方式是 private;
4.2封装—面向对象的三大特性之一
对象的三大特性: 封装,继承,多态
- 什么是封装?
封装: 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
通过访问修饰符(如private)来修饰成员变量和成员方法,隐藏你不想让别人知道的代码,只提供接口让别人使用就行
- 封装的优点:
- 隐藏代码细节,只提供公共访问
- 提到代码复用性
- 提高安全性
5.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员时,需要使用 : : 作用域操作符指明成员属于哪个类域。
class Student
{
public://显示基本信息void Print();private:char _name[20];char _gender[3];int _age;
};//这里需要指定Print是属于Student这个类域,所以需要加上::作用域限定符
void Student::Print()
{cout << _name << " " << _gender << " " << _age << endl;
}
6.类的实例化
打个比方:
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
- 定义一个类就如同定义了一个结构体,跟结构体的自定义类型相似,我们定义类时只是说明了类里面有什么内容,并不能直接使用类的内容,那与定义一个结构体变量就如同类的实例化。
- 这也间接反映出了域里面并不一定都是变量的定义,也有可能都是变量的声明。
class student
{
public:void Print();
private:char name[6];int age;int number;student* p;
};
int main()
{student A;//这样我们就完成了类的实例化。return 0;
}
7.类对象模型
7.1 如何计算类对象的大小
//类中由成员函数和成员变量
class A1
{public:void Print();private:int a;
};// 类中仅有成员函数
class A2
{public:void f2() {}
};//类中仅有成员变量
class A3
{private:int a;
};// 类中什么都没有---空类
class A4
{};int main()
{cout << sizeof(A1) << endl;cout << sizeof(A2) << endl;cout << sizeof(A3) << endl;cout << sizeof(A4) << endl;return 0;
}
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算
一个类的大小?
可以看到仅有成员函数的类和空类都是一个字节,
结论:
- 空类需要占用一个字节来唯一标识这个类的对象(占位,不存储数据)
- 一个类的大小只取决于这个类的“成员变量”之和
- 成员函数大小不计算在内
- 注意计算过程中的结构体内存对其规则
8.this指针
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
8.1 this指针基本认识
class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{Date d1;d1.Init(2023, 1, 1);Date d2;d2.Init(2023, 3, 6);d1.Print();d2.Print();return 0;
}
结果:
2023-1-1
2023-3-6
用 Date 类创建了 d1 和 d2 两个对象,并进行了初始化赋值,那么当我们调用 Print 函数去打印的时候,Print 函数是如何区分 d1 和 d2 对象的呢?
我们看这里的函数调用,为什么不用通过传类变量的地址,直接就能使用类里面的变量?
说明:这里隐含了一个this指针,那this指针是什么呢?
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数(存储在栈帧里),让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。
注:VS中对this指针传递优化,对象地址存在ecx(寄存器)中
只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
8.2 this 指针的特点
-
this指针的类型:
类的类型* const
被 const 修饰以后,this 指针本身不能被修改,但是 this 指针指向的对象可以被修改,还可以进行初始化。 -
this 指针只能在 “成员函数” 的内部使用。
-
this 指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针。
-
this 指针是成员函数第一个隐含的指针形参,存放在栈里面,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
8.3 代码解析
- 代码一
struct A
{
public:void Show(){cout << "Show()" << endl;}int _a;
};int main()
{A* p = NULL;//这里的 p 就相当于一个this指针p->Show();
}
//正常运行
//相当于将空指针穿给函数,这里不对指针操作所以没问题
- 代码二
struct A
{
public:void PrintA(){cout << _a << endl;//这里_a相当于this->_a发生了对空指针解引用的情况}int _a;
};int main()
{A* p = NULL;p->PrintA();//说明这里p->PrintA()等于(*p).PrintA()//但这里并没有语法错误:因为这里是调用里面函数,函数并不占用类的空间,因此可以调用类的函数//但是这里将p传进去了,p是一个空指针,在里面访问变量时,会产生对空指针解引用的情况,因此//具体的报错位置在函数里面的变量使用
}
//运行崩溃
中篇
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,实例化对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
- 函数名与类名相同。
- 无返回值(不用写void)。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数(不传参就能调用),并且默认构造函数只能有一个。
例 1:
#include<iostream>
using namespace::std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)//类名就是函数名,无返回值{_year = year;_month = month;_day = day;}Date(int year, int month = 1, int day = 1)//构成重载{_year = year;_month = month;_day = day;}
private:int _day;int _month;int _year;
};
int main()
{Date A;//自动调用析构函数//Date A(); return 0;
}
全缺省的情况下调用函数应该写成下面注释的代码吗?
我们乍一看好像是的,如果我们换一种形式呢——Date func(); 像不像一个函数声明呢?那为什么不这样写,其实就是为了与函数的声明进行区分!
这里是对象只能这样写!!!
例 2:
#include<iostream>
using namespace::std;
class Date
{
public:
private:int _day;int _month;int _year;
};
int main()
{Date A;//Date A();return 0;
}
这是为什么呢?编译器竟然没有帮我们完成初始化。
语法上对编译器生成的构造函数只是说明了一点:
- 对内置类型不做处理——这里的内置类型指的是编译器原本就有的类型
说明:有的编译器还是会处理的,我们就当做不处理对待。 - 对自定义类型去调用它的默认构造
例 3:
using namespace::std;
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date(){_year = 1;_month = 1;_day = 1;}
private:int _day;int _month;int _year;
};
int main()
{Date A(2023,5,3);//这就跟函数调用差不多了,就多了个类型。return 0;
}
//------------------------
using namespace::std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _day;int _month;int _year;
};
int main()
{Date A(2023,5,3);//这就跟函数调用差不多了,就多了个类型。return 0;
}
总结:
- 一般情况下,内置类型一般都需要写构造函数,不能用编译器生成的。
- 自定义类型我们可以考虑不写构造函数,可以让编译器生成,其本质上是使用自定义类型的默认构造函数。
- 成员变量可以给缺省值。
3.析构函数(需要传参)
3.1 概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 性质
- 函数名为:~ + 类名
- 无参无返回值,不构成重载
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 在对象生命周期结束时 自动调用。
- 自定义类型会自动调用自己的析构函数
3.3 使用情况
一般情况下,有动态申请资源的,且不是自定义类型
例
class Stack
{
public:// 构造函数Stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity);if (_a == nullptr){cout << "malloc fail" << endl;exit(-1);}_top = 0;_capacity = capacity;}// 析构函数~Stack(){free(_a);_a = nullptr;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack st;return 0;
}
4.拷贝构造函数
4.1 概念
只有单个形参,该形参是对 本类类型对象的引用(一般常用const
修饰) ,在用已存在的类类型对象创建新对象时由编译器自动调用。
注: 如其名,在构造时完成对象的初始化拷贝
4.2 特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的
引用
,使用传值方式会引发无穷递归调用,编译器直接报错。
加const
的原因是因为我们怕有些人写错顺序,导致原来的d被改变
//拷贝构造
Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
- 若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
内置类型按照浅拷贝进行拷贝,自定义类型调用它的拷贝构造函数
- 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
class Stack
{
public:Stack(int capacity = 4){_data = (int*)malloc(sizeof(int) * capacity);if (_data == nullptr){cout << "malloc fail" << endl;exit(-1);}_top = 0;_capacity = capacity;}~Stack(){free(_data);_data = nullptr;}
private:int* _data;int _top;int _capacity;
};int main()
{Stack st1;Stack st2(st1);return 0;
}
我们用的是编译器自动生成的拷贝构造函数,进行调试我们可以看到:st2已经完成了对st1的拷贝
深拷贝
#include<iostream>
using namespace::std;
class Stack
{
public:Stack(int capacity = 4){cout << "Stack()" << endl;int* tmp = (int*)malloc(sizeof(int) * capacity);if (tmp == NULL){perror("malloc fail");exit(-1);}else{_arr = tmp;_top = 0;_capacity = capacity;}}Stack(const Stack& A) //深拷贝{int* tmp = (int*)malloc(sizeof(int) * A._capacity);if (tmp == NULL){perror("malloc fail");exit(-1);}else{memcpy(tmp, A._arr, sizeof(int) * A._capacity);_arr = tmp;_top = A._top;_capacity = A._capacity;}}void PushBack(int data){_arr[_top++] = data;}~Stack(){cout << "~Stack()" << endl;free(_arr);_arr = NULL;}
private:int* _arr;int _top;int _capacity;
};
int main()
{Stack s1;Stack s2(s1);return 0;
}
- 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象(函数返回的是临时对象,临时对象具有常性,有时需要注意一下权限的问题)
5.赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
运算符重载和函数重载是不一样的
- 函数重载:支持函数名相同、参数不同的函数,可以同时使用
- 运算符重载:自定义类型对象可以使用运算符
基本特征
- 目的:增强代码可读性
- 基本特征:
- 具备返回类型与参数以及返回值
- 函数名为operate+操作符(必须是已经存在的!)
- 注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.*
::
sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
下面以 d1==d2为例,通过代码的形式来学习运算符重载
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1) // 构造函数{_year = year;_month = month;_day = day;}//==的运算符重载函数bool operator==(const Date& d) // 这里等价于 bool operator==(Date* this, const Date& d2){// 这里需要注意的是,左操作数是this指向的调用函数的对象return _year == d._year&& _month == d._month&& _day == d._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 5, 1);Date d2(2021, 3, 1);//显示调用:d1.operator==(d2);//我们一般就直接这样写d1 == d2;//同上,编译器会自动识别转换为 d1.operator==(d2) --> d1.operator(&d1, d2);cout << (d1 == d2) << endl; // 注意运算符重载打印时要加括号(),因为运算符优先级的关系return 0;
}
当然,我们也可以将运算符重载放到类外,但这样就无妨访问到收private私有类型保护的成员变量,我们可以用将成员变量设置为public的公共类型,这样当然是不好的,等到后面学习了友元函数就可以解决
我们先将成员函数设为public来看一看,放到类外的运算符重载,代码应该怎么写
class Date
{
public:Date(int year = 1, int month = 1, int day = 1) // 构造函数{_year = year;_month = month;_day = day;}//private: //将成员函数变为公共类int _year;int _month;int _day;
};bool operator==(const Date& d1, const Date& d2)// ==运算符重载函数
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1(2023, 5, 1);Date d2(2021, 3, 1);//显示调用operator==(d1, d2);//一般写法:d1 == d2; //同上,如果没有重载会报错,如果重载了它会转换为 operator==(d1, d2);cout << (d1 == d2) << endl;return 0;
}
可以看到在类外时,因为没有 this 指针,所以再显示调用函数时,其形参我们需要设置两个
放到类外的写法是不推荐的,因为他破坏了封装,这里只是为了让大家看看类内写法和类外写法的区别,以及理解隐藏的this参数
在编写好运算符重载函数后,无论它是在类内还是类外,我们写的时候都是直接写 d1 == d2
这里我们可以在写一下其他的比较,比如 < , > , <=, >= , !=
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1) // 构造函数{_year = year;_month = month;_day = day;}//==的运算符重载函数bool operator==(const Date& d) {return _year == d._year&& _month == d._month&& _day == d._day;}// d1 < d2bool operator<(const Date& d){if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else if (_year == d._year && _month == d._month && _day < d._day){return true;}else{return false;}}// d1 <= d2bool operator<=(const Date& d){return *this < d || *this == d;}// d1 > d2bool operator>(const Date& d){return !(*this <= d);}// d1 >= d2bool operator>=(const Date& d){return !(*this < d);}// d1 != d2bool operator!=(const Date& d){return !(*this == d);}private:int _year;int _month;int _day;
};
我们单独把这里的重载函数列出来:
bool operator== (const Date& B)
//其实就是bool operator== (Date *const this,const Date& B)
{
return _day == B._day&& _month == B._month&& _year == B._year;
}
- 这里需要A==B,那么就相当于 A.operator==(B)
- 表面上缺少了一个参数其实是this指针。
- 因此:比较n个操作数我们只需要传进去n-1个参数即可。
5.2 赋值运算符重载
赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
赋值运算符重载函数与拷贝构造函数的区别:
- 拷贝构造 — 用一个已存在的对象初始化另一个对象
- 赋值运算符重载 — 已存在的两个对象之间的复制
以日期Date类为例:
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;}//赋值运算符重载Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}private:int _year;int _month;int _day;
};
至于我们为什么要这样写赋值运算符重载,有以下原因:
原因1:
为什么函数要写返回值?并且函数的返回值要使用 引用返回 ?
写返回值是因为,在赋值操作的时候我们经常会遇到连续赋值的场景,如
d1 = d2 = d3 = d4
写返回值就能实现连续赋值的情况,更加模拟赋值运算符
用引用返回是为了避免不必要的拷贝,适用于出来作用域返回值没有被销毁的场景
(*this 即d4 在出函数后未销毁,但一旦成员函数执行结束,this指针的作用域也随之结束)
原因2:
为什么要用 if ,他是用来干嘛的?
这个 if 条件判断是用来检查,赋值是否是给自己赋值的,
因为我们在赋值操作时有时会写成:d1 = d1 的情况为了防止不必要的操作
原因3:
为什么返回的是 *this?
对于 赋值运算 d1 = d2 的情况,是将d2的值赋值给d1
对其显示处理就是:d1.operator = (d2)
又因为赋值运算的计算顺序是从右往左走的,在连续赋值的时候就会有所体现
所以我们应该返回赋值运算的左操作数,而在类中,左操作数就是this指针
this是d1的地址,返回*this,就是返回d1
5.3 赋值运算符重载的特性
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意: 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
注!!!:Date tmp = *this;
是拷贝构造
6.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 << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{const Date d1(2022, 9, 1);//用const修饰对象d1,使其成员在使用时不会改变d1.Print();return 0;
}
以上代码中,我们用const
来修饰类对象d1
,使得d1
里面的成员变量不会再之后的使用中被修改
但是再编译时会报错:
很明显我们的Print
函数没有去该改变成员变量,那为什么还会报错呢?
这是因为,在 d1
对象去调用 Print
函数的时候,实参会把 d1
的地址传过去,但是 d1
是被 const
修饰的,也就是传过去的是 const Date*
;
而在 Print1
函数这边,形参部分会有一个隐含的 this 指针,也是 Date* this
也就是把 const Date*
传给了 Date* this
,在这里属于权限的放大,所以编译会不通过。
这里我们就可以用const
来修饰成员函数:把 const
放在成员函数(声明和定义)之后,实际就是修饰 this
指针:
void Print() const
{cout << _year << _month << _day << endl;
}
适用场景:
- 传参对象有const修饰。
- 只要函数内部不对成员变量进行修改。
7.取地址及const取地址操作符重载
取地址操作符重载 和 const 取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行。
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//显示写出:取地址操作符重载//普通对象 取地址操作符重载Date* operator&(){return this;}//const对象 取地址操作符重载const Date* operator&() const{return this;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 5, 7);const Date d2(2023, 5, 7);cout << &d1 << endl;cout << &d2 << endl;return 0;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,我们作为了解、知道有这两个东西存在即可
只有特殊情况,才需要重载,比如想让别人获取到指定的内容,就可以自己实现。
下篇
1.再谈构造函数
1.1 函数体内赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
这里需要注意:
上述代码再构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对象成员变量的初始化,
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
理由如下代码:
class Date
{
public:Date(int year, int month, int day){_year = year; // 第一次赋值_year = 2022; // 第二次赋值//...还可以赋值很多次_month = month;_day = day;}
private:int _year;int _month;int _day;
};
这段代码中,构造函数对_year
的多次复制,其编译过程是被允许的。
那么既然构造函数里面进行的是赋值的话,什么时候才是成员变量的初始化?
这就要引出我们真正的初始方式:初始化列表
1.2 初始化列表(构造函数的一部分)
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public://初始化列表是对成员函数的初始化,他直接跟在析构函数参数括号的后面Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};int main()
{Date d1;// 类对象的整体初始化
//(初始化了类对象,但没有初始化成员变量),成员变量放到了初始化列表里面解决return 0;
}
- 因此:初始化列表是成员定义的地方
- 注意: 一个成员最多初始化一次,也就是说有的成员是可以不用显式的写在初始化列表,但是有的成员是必须显式的出现在初始化列表中。——总结关键:显式
- 说明:必须显式的出现在初始化列表的成员有:
- 引用
- const修饰的变量
- 自定义类型不是默认构造函数(全缺省,无参,不写构造函数)的
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,
与其在初始化列表中的先后次序无关
int i = 0;class Test
{
public:Test():_b(i++), _a(i++){}void Print(){cout << "_a:" << _a << endl;cout << "_b:" << _b << endl;}
private:int _a;int _b;
};int main()
{Test test;test.Print();return 0;
}
所以尽量让声明的顺序与初始化的顺序保持一致
1.3 explicit关键字
隐式类型转换
class Date
{
public:Date(int year = 1){_year = year;}//拷贝构造Date(Date& B){_year = B._year;}void Print() const//D类型为const因此this指针也必须为const类型的 {cout << _year << endl;}
private:int _year;
};
int main()
{Date A;//调用默认的拷贝构造Date B = A;//调用的是拷贝构造,本质上为Date B(A);Date C = 1;//隐式类型转换,先将1转换为一个Date类型的临时变量,再将临时变量拷贝到C中//但是在编译器运行时,为了提升效率会将这两步合二为一调用构造函数。//需要特别注意的是——这里的1发生隐式类型转换的条件为,得有合适的拷贝构造。const Date& D = 1;//这里也发生隐式类型转换,将1转换为Date类型的临时变量,//不要忘记这里的临时对象具有常属性!//因此要加上const,但这里只是个临时变量,那是否会销毁呢?//打印一下D.Print();return 0;
}
程序运行结果:
1
很显然这里的临时变量没有被销毁 。
临时对象的生命周期确实会被延长到引用 D
的生命周期结
有没有方法不让隐式类型转换发生呢?
答案显然是有的,那就是在构造函数的前面加上explicit
class Date
{
public:explicit Date(int year = 1){year = 1;}//拷贝构造Date(Date& B){_year = B._year;}void Print() const{cout << _year << endl;}
private:int _year;
};
int main()
{Date A;Date B = A;Date C = 1;const Date& D = 1;D.Print();return 0;
}
编译器将在编译的阶段就阻止了我们发生隐式类型转换。
2.static成员
2.1 概念
声明为static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量;
用static
修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化.
calss A
{// 静态成员函数static int func() { }// 静态成员变量static int _scount;
}
2.2 特性
特性一:静态成员为所有类对象所共享,不属于某个具体的实例
class Test1 { private:int _n; };class Test2 { private:int _n;static int _aaa; }; int main() {cout << sizeof(Test1) << endl;cout << sizeof(Test2) << endl;return 0; }
结果:
4 4
Test2
的类里面比Test1
类多定义了一个静态变量_aaa
但是其空间并没有增加
因为静态成员_aaa
是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。
特性二:静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
class Date { private://静态成员变量的声明static int _year;static int _month;static int _day; };// 静态成员变量的定义初始化,不经过初始化列表 int Date::_year = 2023; int Date::_month = 5; int Date::_day = 8;
即使是私有的变量也要在外部定义
特性三:类静态成员(共有)即可用 类名::静态成员函数(非静态无法做到) 或者 对象.静态成员函数 来访问
class Date { public:static void Print(){cout << _year << "-" << _month << endl;} private:static int _year;static int _month; };int Date::_year = 2022; int Date::_month = 10;int main() {Date d1;d1.Print(); // 对象.静态成员函数Date::Print(); // 类名::静态成员函数return 0; }
特性四:静态成员函数没有隐藏的
this
指针,不能访问任何非静态成员class Date { public:static void Print(){cout << _year << endl; //静态的成员可以访问cout << _month << endl; //不能访问非静态成员} private:static int _year;int _month; };int Date::_year = 2022;int main() {Date d1;d1.Print(); // 对象.静态成员return 0; }
静态成员函数属于类本身,而不是类的某个具体对象。因此,它不能访问类的非静态成员变量和非静态成员函数(因为这些成员需要一个具体的对象才能访问)。
含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
特性五:静态成员也是类的成员,受public、protected、private 访问限定符的限制
在静态成员变量设为private时,经过我们通过类域进行访问,也是不行的
访问静态成员变量的方法:
class Date { public:static int GetMonth(){return _month;}static int _year; private:static int _month; };//静态成员变量的定义初始化 int Date::_year = 2023; int Date::_month = 5;int main() {Date d1;//对公共属性的静态成员进行访问cout << d1._year << endl; // 1.通过类对象突破类域进行访问cout << Date()._year << endl; // 2.通过匿名对象突破类域进行访问cout << Date::_year << endl; // 3.通过类名突破类域进行访问//对私有属性的静态成员进行访问cout << d1.GetMonth() << endl; // 1.通过类对象突破类域进行访问cout << Date().GetMonth() << endl; // 2.通过匿名对象突破类域进行访问cout << Date::GetMonth() << endl; // 3.通过类名突破类域进行访问return 0; }
3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
3.1 友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用
const
修饰(因为没有this
指针) - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
友元类的所有成员函数全是另一个类的友元函数,都可以访问另一个类中的非公有成员。
friend class Date; // 声明日期类为友元类
特性:
- 友元关系是单向的,不具有交换性。
比如上述
Time
类和Date
类,在Time
类中声明Date
类为其友元类,那么可以在Date
类中直接访问Time
类的私有成员变量,但想在Time
类中访问Date
类中私有的成员变量则不行。
- 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
- 友元关系不能继承
4.内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类
class A
{
public:// 内部类class B // B天生就是A的友元,在B里面可以直接访问A的私有成员{public:void Print(const A& a){cout << y << endl; // 可以直接访问静态成员cout << a.h << endl; // 也可以访问普通成员}private:int _b;};private:static int y;int h;
};int A::y = 1;int main()
{A a; // 定义a对象A::B bb; // 定义bb对象bb.Print(a); // 把a对象传给bb对象,打印return 0;
}
上述代码中,我们在A类里面定义了一个B类,B就叫做A的内部类
B天生就是A的友元,B可以访问A的私有,但不能访问B的私有
注意:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
5.匿名对象
对于一个类 class A{ }
;
通过前面的学习,我们知道,在定义类对象时,如果不传对象不能这样写:A aa1();
但是我们可以写成这样: A();
特点:
- 匿名对象的声明周期只有写匿名周期的那一行
class A { public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}private:int _a; };int main() {A aa1; A();return; }
匿名对象在离开它那一行的时候,它的生命周期就结束了
既然匿名对象的周期很短,那能不能延长它的生命周期呢?
有的,有的——const
引用
int main()
{const Date& date = Date(1234);date.Print();//这里所传的this指针是const Date* const 类型的,因此Print函数需要用const修饰一下。return 0;
}
6.拷贝对象时的一些编译器优化
编译器通常会将连续的构造进行优化。
class Date
{
public:Date(int year = 1949):_year(year){cout << "Date()" << endl;}Date(const Date& B){_year = B._year;}~Date(){cout << "~Date()" << endl;}
private:int _year;
};
Date Func()
{Date A;//返回时先拷贝构造再析构return A;}
void Func1(Date A)
{}
int main()
{//这里是一个拷贝构造+拷贝构造优化为拷贝构造Date A = Func();//不会优化Date A;Func(A);return 0;
}
练习
1.Date代码
Date.h
#pragma once
#include<iostream>
using namespace std;class Date
{//流插入(友元函数声明)friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& out, Date& d);
public:Date(int year = 1, int month = 1, int day = 1);void Print() const{cout << _year << "-" << _month << "-" << _day << endl;}int GetMonthDay(int year, int month);//日期比较bool operator<(const Date& d) const;bool operator==(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator!=(const Date& d) const;Date& operator+=(int day);Date operator+(int day) const;Date& operator-=(int day);Date operator-(int day) const;Date& operator++(); //前置++Date operator++(int);//后置++Date& operator--();Date operator--(int);//日期-日期int operator-(const Date& d);private:int _year;int _month;int _day;
};
Date.cpp
#include "Date.h"Date::Date(int year, int month, int day)
{if (0 < month && month < 13&& 0<day && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" <<year << "-" << month << "-" << day << endl;//assert(false);}}bool Date::operator<(const Date& d) const
{if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else if (_year == d._year && _month == d._month && _day < d._day){return true;}else{return false;}
}bool Date::operator==(const Date& d) const
{return _year == d._year&& _month == d._month&& _day == d._day;
}bool Date::operator<=(const Date& d) const
{return *this < d || *this == d;
}bool Date::operator>(const Date& d) const
{return !(*this <= d);
}bool Date::operator>=(const Date& d) const
{return !(*this < d);
}bool Date::operator!=(const Date& d) const
{return !(*this == d);
}int Date::GetMonthDay(int year, int month)
{static int dayArr[13] = { 0,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 dayArr[month];}
}Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;/*tmp._day += day;while (_day > GetMonthDay(tmp._year, tmp._month)){_day -= GetMonthDay(tmp._year, tmp._month);tmp._month++;if (tmp._month > 12){tmp._year++;tmp._month = 1;}}return tmp;*/
}Date& Date::operator+=(int day)
{if (day < 0){return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_year++;_month = 1;}}return *this;/**this = *this + day;return *this;*/
}// 日期-天数Date Date::operator-(int day) const
{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){_month = 12;_year--;}_day += GetMonthDay(_month, _year);}return *this;
}Date& Date::operator++()
{return *this += 1;
}Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}Date& Date::operator--()
{return *this -= 1;
}Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return *this;
}int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++min;++n;}return n * flag;
}ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, Date& d)
{int year, month, day;in >> year >> month >> day;if (0 < month && month < 13&& 0 < day && day <= d.GetMonthDay(year, month)){d._year = year;d._month = month;d._day = day;}else{cout << "非法日期:" << year << "-" << month << "-" << day << endl;//assert(false);}return in;
}
2.OJ题
2.1 求1+2+3+…+n(不用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
/**思路:* 1. 题目要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句A?B:C说明不能采用常规方法进行累加求和* 2. 充分利用C++特性,构造对象构造函数会自动调用的特点,让求和在构造函数中进行完成* 3. 由于所有对象要针对相同的和进行更新,所以其成员定义为static*/class Solution
{public:class Sum//内部类{public:Sum(){_sum += _i;_i++;}};int Sum_Solution(int n) {_i = 1;_sum = 0;Sum array[n];return _sum;}static size_t _sum;static size_t _i;
};size_t Solution::_sum = 0;
size_t Solution::_i = 1;
2.2 计算一年的第几天
#include<iostream>
using namespace std;
/**思路:* 1. 通过枚举每个月的1号是这一年的第几天,从而进行累加求和即可,其中注意闰年的处理*/int main(){int year, month, day;while(cin>>year>>month>>day){int monthDays[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};int nday = monthDays[month-1] + day;if(month > 2 &&((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)){nday += 1;}cout<<nday<<endl;}return 0;
}
自己写的
#include <iostream>
using namespace std;int main()
{int year, month,day;cin >> year >> month>>day;int dayArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)){dayArr[2]=29;}int tmpmon=1,sum=0;while(tmpmon<month){sum+=dayArr[tmpmon];tmpmon++;}sum+=day;cout<<sum;
}
2.3 日期差值与打印日期
#pragma once
#include<iostream>
using namespace std;class Date
{//流插入(友元函数声明)friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& out, Date& d);
public:Date(int year = 1, int month = 1, int day = 0);void Print() const{//cout << _year << "-" << _month << "-" << _day << endl;printf("%d-%02d-%02d\n",_year,_month,_day);}int GetMonthDay(int year, int month);//日期比较bool operator<(const Date& d) const;bool operator==(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator!=(const Date& d) const;Date& operator+=(int day);Date operator+(int day) const;Date& operator-=(int day);Date operator-(int day) const;Date& operator++(); //前置++Date operator++(int);//后置++Date& operator--();Date operator--(int);//日期-日期int operator-(const Date& d);//private:int _year;int _month;int _day;
};Date::Date(int year, int month, int day)
{if (0 <= month && month < 13&& 0<=day && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" <<year << "-" << month << "-" << day << endl;//assert(false);}}bool Date::operator<(const Date& d) const
{if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else if (_year == d._year && _month == d._month && _day < d._day){return true;}else{return false;}
}bool Date::operator==(const Date& d) const
{return _year == d._year&& _month == d._month&& _day == d._day;
}bool Date::operator<=(const Date& d) const
{return *this < d || *this == d;
}bool Date::operator>(const Date& d) const
{return !(*this <= d);
}bool Date::operator>=(const Date& d) const
{return !(*this < d);
}bool Date::operator!=(const Date& d) const
{return !(*this == d);
}int Date::GetMonthDay(int year, int month)
{static int dayArr[13] = { 0,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 dayArr[month];}
}Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;/*tmp._day += day;while (_day > GetMonthDay(tmp._year, tmp._month)){_day -= GetMonthDay(tmp._year, tmp._month);tmp._month++;if (tmp._month > 12){tmp._year++;tmp._month = 1;}}return tmp;*/
}Date& Date::operator+=(int day)
{if (day < 0){return *this -= -day;}_day += day;//Print();while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month > 12){_year++;_month = 1;}}return *this;/**this = *this + day;return *this;*/
}// 日期-天数Date Date::operator-(int day) const
{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){_month = 12;_year--;}_day += GetMonthDay(_month, _year);}return *this;
}Date& Date::operator++()
{return *this += 1;
}Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}Date& Date::operator--()
{return *this -= 1;
}Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return *this;
}int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++min;--max;}return n * flag;
}ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, Date& d)
{int year, month, day;in >> year >> month >> day;if (0 <= month && month < 13&& 0 <= day && day <= d.GetMonthDay(year, month)){d._year = year;d._month = month;d._day = day;}else{cout << "非法日期:" << year << "-" << month << "-" << day << endl;//assert(false);}return in;
}int main()
{//1.日期差值int year,month,day;while(scanf("%4d%2d%2d",&year,&month,&day)!=EOF){Date d1(year,month,day);scanf("%4d%2d%2d",&year,&month,&day);Date d2(year,month,day);cout<<(d2-d1)+1<<endl;}//2.打印日期// int year,day;// while(cin>>year>>day)// {// Date d(year,1,0);// d+=day;// d.Print();// }}