目录
一、类的6个默认成员函数
1.默认成员函数概念
2.默认成员函数分类
二、C++提出构造函数、析构函数的背景
1.构造函数的提出背景
2.析构函数的提出背景
3.案例分析
三、构造函数
1..构造函数概念
2..构造函数特性
2.1.特性1:构造函数的函数名与类名相同。
2.2.特性2:构造函数无返回值。
2.3.特性3:对象实例化时编译器自动调用对应的构造函数对对象进行初始化。
2.4.特性4:构造函数可以重载(相当于1个类可以有多个构造函数,也相当于1个类可以有多个初始化方式)。
2.4.1.对象调用无参构造函数初始化
(1)调用格式:
(2)案例:
2.4.2.对象调用带参构造函数初始化(不是全缺省参数)
(1)调用格式:
(2)案例:
2.4.3.对象调用全缺省参数的构造函数初始化
案例:日期类
(1)正确代码
(2)错误代码:
2.5.特性5:如果类中没有自定义实现构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户自定义实现构造函数则编译器将不再默认生成构造函数。
2.5.1.默认构造函数
(1)编译器自动生成的默认构造函数
(2)自定义默认构造函数的类型解析
2.5.2.下面展示了,若我们自己不写构造函数则编译器自己回生成默认构造函数;若我们自己实现构造函数则编译器会不会生成默认构造函数。
(1)案例1:编译器生成默认构造函数
(2)案例2:编译器没有生成默认构造函数
2.6.特性6:编译器生成的默认构造函数对于自定义类型的成员变量只会调用它的默认构造函数(即不用传参的默认构造函数,例如无参、全缺省、编译器生成),但是对于内置类型的成员变量是不做处理的。
2.6.1.编译器生成的默认构造函数出现的问题:不对内置类型的成员变量进行初始化。
2.6.2.对于编译器生成的默认构造函数无法对内置类型的成员变量进行初始化的问题,以下是解决方式。
2.7.特性7:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
2.7.1.结论
3.编译器自动生成的默认函数的引用场景
3.1.案例1:用两个栈实现队列
四、析构函数
1.析构函数概念
2.析构函数特性
2.1.析构函数名是在类名前加上字符 ~。
2.2.无参数无返回值类型。
2.3.一个类只能有一个析构函数。若未自定义析构函数,系统会自动生成默认的析构函数。注意:析构函数不能重载。
2.4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
2.5.编译器生成的默认析构函数,对自定类型成员调用它的析构函数(即编译器生成、自定义实现)。
2.6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3.析构函数使用案例
3.1.案例1:有效的括号
C++提出拷贝构造函数背景
五、拷贝构造函数
1.拷贝构造函数概念
2.拷贝构造函数特性
2.1.特性1:拷贝构造函数是构造函数的一个重载形式。
2.2.特性2:拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
1.由于编译器会自动生成默认拷贝构造函数对自定义类型的类对象进行值拷贝(浅拷贝),但为什么还要我们自定义实现拷贝构造函数呢?对于成员变量包含指向动态内存分配的指针的类对象,进行值拷贝(浅拷贝)可能带来哪些危害?
1.1.编译器默认情况下会对自定义类型的类对象进行拷贝操作,但这通常是执行浅拷贝。
1.2.浅拷贝的危害
1.3.案例:类Stack的成员变量包含
1.4.总结
2.为什么程序员在自定义实现拷贝构造函数时,通常不建议使用传值传参方式来实现拷贝构造函数?拷贝构造函数采用传值传参会无穷递归的原因?
3.传引用传参Date(const Date& d)的形参d用const修饰的原因:
4.拷贝构造函数的两种实现方式:传引用传参、传指针传参
5.使用一个已存在的对象初始化一个新对象的两种方法
6.总结
2.3.特性3:若未显式定义(即未自定义拷贝构造函数),编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
1.编译器自动生成的默认拷贝构造函数
1.1.编译器生成的默认拷贝构造函数是会对内置类型数据进行处理
(1)案例1:日期类
(2)案例2:栈类
1.2.编译器生成的默认拷贝构造函数对自定义类型数据的处理方式是:调用它的拷贝构造函数。
(1)案例1:用两个栈实现队列
2.默认拷贝构造函数
3.总结
3.拷贝构造函数典型调用场景
4.拷贝构造使用案例
案例1:实现一个计算给定日期向后推算指定天数的新日期的功能
六、运算符重载函数(运算符重载)
1.C++提出运算符重载的作用
1.1.运算符重载作用
1.2.对作用2进行解析
(1)对于运算符的操作数类型,编译器会自动识别内置类型的操作数,而无法识别自定义类型的操作数?为什么内置类型可以直接使用运算符,而自定义类型不可以直接使用运算符?
2.运算符重载函数介绍
2.1.运算符重载函数概念
2.2.运算符重载函数的格式
2.3.operator==判断相等运算符重载函数的实现
2.4.operator
2.5.operator>、operator>=、operator
3.运算符重载注意事项
3.1.不能通过连接其他符号来创建新的操作符:比如operator@
3.2.重载操作符必须有一个类类型参数
3.3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
3.4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
3.5. .* :: sizeof ?: .注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
七、赋值运算符重载函数(赋值重载)
1.日期类Date& operator=(const Date& d)运算符重载函数实现过程
1.1.operator=(const Date d)赋值运算符重载函数可以传值传参
(1)赋值运算符重载函数可以传值传参而不引发无穷递归的原因:
(2)赋值运算符重载函数使用传值传参、传值返回的缺陷
1.2.Date& operator=(const Date& d)赋值运算符重载函数用传引用传参、传引用返回实现过程:
(1)代码1:void operator=(const Date& d)
(2)代码2:Date operator=(const Date& d)
(3)代码3:Date& operator=(const Date& d)
2.赋值运算符重载格式
3.赋值运算符只能重载成类的成员函数不能重载成全局函数
4. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
4.1.编译器自动生成的默认赋值运算符重载函数
4.2.拷贝构造和赋值重载的判断
八、日期类的实现
日期类成员函数的解析
1.日期+=天数:Date& operator+=(int day)
1.1.日期+=天数的思路
1.2.写法1
(1)代码
(2)代码解析
(3)测试
(4)结论
1.3.写法2
1.3.写法3
2.日期+天数:Date operator+(int day)
2.1.写法1
(1)代码
2.2.错误代码
(1)代码
(2)解析即使operator+内部的临时对象被static修饰但是operator+也不能用引用返回的原因
2.3.写法2
(1)代码
3.对实现operator+、operator+=两种方式进行总结
3.1.方式1:先自定义实现operator+,然后operator+=函数内部复用operator+从而实现operator+=函数。
3.2.方式2:先自定义实现operator+=,然后operator+函数内部复用operator+=从而实现operator+函数。
3.3.总结
4.operator++函数的实现
4.1.前置++运算符重载函数
(1)实现思路
(2)代码
4.2.后置++运算符重载函数
(1)实现思路
(2)代码
4.3.前置++效率高,还是后置++效率高?
4.4.编译器调用前置++、后置++的方式
5.operator-=、operator-函数的实现
5.1.日期-=天数 Date& operator-=(int day) 的函数实现
5.1.1.思路1
(1)思路
(2)代码
5.1.2.思路2
(1)思路
(2)代码
(3)测试
(4)代码解析
5.2.日期-天数 Date& operator-(int day)的函数实现
(1)思路
(2)代码
5.3.日期-日期 int operator-(const Date& d)的函数实现
(1)思路
(2)代码
(3)测试
6.operator--函数的实现
6.1.前置--
6.2.后置--
6.3.总结
7.插入数据(右操作数)是自定义类型(例如:对象)的流插入'
7.1.知识点
7.2.解释 C++ 中 cout
7.3.猜想自定义类型插入数据(右操作数)的流插入运算符重载函数在什么位置定义实现比较好?
7.3.1.猜想1:声明和定义成类的成员函数,即void Date::operator
(1)代码实现
(2)错误调用方式导致发生报错
(3)正确调用方式没有发生报错
(4)方式1的缺陷
7.3.2.猜想2:声明和定义成类的非成员函数(即在全局域中定义实现),ostream& operator
ostream& operator
7.4.C++支持流插入运算符重载函数的原因
8.插入数据(右操作数)是自定义类型(例如:对象)的流提取'>>'运算符重载函数
8.1.istream& operator>>(istream& in, Date& d)的实现过程
9.把流插入运算符重载函数、流提取运算符重载函数声明和定义成内联函数
9.1.对于流插入运算符重载函数、流提取运算符重载函数的优化
(1)优化方式
(2)优化原因
(3)注意事项
(4)优化后的代码
9.2.详细说明内联函数声明和定义不能分离的原因
(1)类的声明和定义
(2)源文件中函数声明与定义的关系及其对调用和链接过程的影响
(3)对于声明和定义分离的非内联函数来说,符号表的作用是什么?
(4)内联函数的介绍
(5)内联函数为什么不入符号表的原因
(6)内联函数不入符号表导致内联函数声明和定义分离时会发生编译报错的原因(或者说内联函数声明和定义不能分离的原因)
日期类整个工程
1.1.Date.h
1.2.Date.cpp
1.3.Test.cpp
九、const成员函数
1.C++提出const成员函数的背景
1.1.权限放大代码
(1)代码报错的原因
(2)解决方式
1.2.权限缩小代码
2.const成员函数的介绍
2.1.const成员函数定义
2.2.类成员函数是否加const修饰*this的判断方式
2.2.1.判断方法
2.2.2.案例
3.思考下面的几个问题
3.1. const对象可以调用非const成员函数吗?
3.2. 非const对象可以调用const成员函数吗?
3.3. const成员函数内可以调用其它的非const成员函数吗?
3.4. 非const成员函数内可以调用其它的const成员函数吗?
十、取地址运算符重载函数、const取地址运算符重载函数
1.编译器自动生成两种默认取地址运算符重载函数
2.自定义实现两种取地址运算符重载函数
3.两种取地址运算符重载函数的调用
3.1.取地址运算符重载函数A* operator&()的调用
3.2.const取地址运算符重载函数const A* operator&() const的调用
4.两种取地址运算符重载函数A* operator&()、const A* operator&() const 的区别
4.1.*this是否被const修饰
4.2.返回值不同
十一、下标运算符‘[]’的运算符重载函数operator[](int i)的实现
1.int& operator[](int i)的实现
2.const int& operator[](int i)的实现
3.int& operator[](int i)、const int& operator[](int i)可以构成函数重载
一、类的6个默认成员函数
注意:如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
1.默认成员函数概念
(1)概念:在类定义时若程序员没有自定义实现且是由编译器默认自动生成的成员函数称为默认成员函数。
2.默认成员函数分类
(1)编译器自动生成默认成员函数一共有6种,如下图所示:
二、C++提出构造函数、析构函数的背景
1.构造函数的提出背景
在C++出现之前,C语言程序员需要手动初始化对象的数据成员。这个过程容易出错,尤其是在对象较为复杂时,程序员可能会忘记初始化某些成员变量,导致程序运行时出现不可预测的行为。
C++引入了构造函数的概念,其目的是为了确保对象在创建时自动进行初始化。构造函数是一种特殊的成员函数,它在对象实例化时自动被调用,用于执行初始化操作。这样,程序员只需在构造函数中定义初始化逻辑,就可以保证每次创建对象时,其成员变量都会被正确地初始化。
2.析构函数的提出背景
在C++之前,管理动态分配的内存和其他资源是一个容易出错的过程。程序员需要在不再需要这些资源时手动释放它们,以避免内存泄漏和其他资源管理问题。
C++通过引入析构函数来解决这个问题。析构函数是另一个特殊的成员函数,它在对象生命周期结束时自动被调用,用于执行清理操作,如释放动态分配的内存和其他资源。析构函数的引入简化了资源管理,减少了因忘记释放资源而导致的错误。
总计:为了解决我们忘记对对象进行初始化的问题和忘记销毁对象向内存申请的资源(即动态空间),C++才提出构造函数、析构函数。
3.案例分析
(1)案例
①问题1:忘记初始化
在C++中定义了一个Stack
类的对象st
后,如果忘记调用Init
函数来初始化栈,那么直接对栈进行操作(如Push
函数)可能会导致未定义行为,因为成员变量a
可能是一个空指针,对空指针解引用将导致程序崩溃。
②问题2:忘记销毁
在使用完Stack
类的对象后,如果没有调用Destroy
函数来释放动态分配的内存,将导致内存泄漏。特别是在程序结束时,忘记手动调用销毁函数是常见错误。而且还有一个很麻烦的地方是当很多地方要用Destroy销毁(数据结构)时若我们常常会忘记。
(2)问题解决方式:
为了解决上述问题,C++引入了构造函数和析构函数的概念:
-
构造函数:构造函数在对象创建时编译器会自动被调用,用于执行初始化操作。在
Stack
类中,我们可以定义一个构造函数来自动分配内存并设置初始状态。 -
析构函数:析构函数在对象生命周期结束时编译器会自动被调用,用于执行清理操作。在
Stack
类中,析构函数可以用来释放之前分配的内存。
三、构造函数
1..构造函数概念
构造函数是类中用于初始化对象的一个特殊成员函数。它的名称与类名一致,并且在创建类的实例时,编译器会自动调用构造函数来执行对象的初始化操作。构造函数的主要职责是为对象的成员变量赋予初始值,确保对象在诞生之初就处于一个有效状态。值得注意的是,构造函数在整个对象的生命周期内仅被调用一次,即在对象创建的时刻,而在对象即将被销毁之前不会再调用构造函数。这个过程确保了每个成员变量在对象的使用期间都有一个合适的初始值。
注意:
①构造函数不是创建对象而是用来初始化对象的,即构造函数的作用相等于初始化函数Init一样对类的所有成员变量进行初始化。
②构造函数是用来初始化对象的,构造函数的本质工作确实是对对象的所有成员变量进行初始化。当创建一个类的实例时,构造函数会被自动调用,以便为对象的成员变量设置初始值,确保对象在可以使用之前处于一个定义良好的状态。
//自定义实现的构造函数案例
//.cpp
#include<iostream>
using namespace std;
class Date
{
public:
//注意:无论是无参构造函数还是带参构造函数都是我们自定义实现构造函数,所以编译器不会自动
//生成默认构造函数。
//1.无参构造函数
Date()
{}
//2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1;//调用无参构造函数
Date d2(2015, 1, 1);//调用带参的构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
//以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
//warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义