目录
前言
C++类在设计之时,规定类中有六个默认的成员函数,这些成员函数天生就存在,而且功能都很强大,类和对象的关键点就在这六个默认成员函数的学习,本篇将会逐一介绍这六个成员函数,让我们向类和对象的深处出发!
正文
C++规定在每个类中有六个默认函数成员:
函数 功能 重要性 构造函数 定义和初始化成员变量 重要 析构函数 释放申请的内存空间(销毁成员变量) 重要 拷贝构造(函数) 实现对象间的深拷贝 重要 赋值重载(函数) 实现对象间的深赋值 重要 取地址重载(函数) 自定义类对象取地址操作符功能 一般 const取地址重载(函数) 自定义对象取地址操作符功能const修饰返回的地址 一般 这些函数我们不写,编译器也会自己写一个默认的函数代替对应函数!
//以日期类的方式初见六大默认成员函数
class Date
{
public:
//构造函数
Date(size_t year = 1970, size_t month = 1, size_t day = 1)
{
_year = year;
_month = month;
_day = day;
}
//析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//拷贝构造函数(简称:拷贝构造)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值重载函数(赋值运算符重载)
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//取地址运算符重载
Date* operator&()
{
return this;
}
//const返回取地址运算符重载,const修饰this
const Date* operator&() const
{
return this;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
构造函数
对比C和C++的初始化
我们在使用C语言实现一些例如顺序表,栈等简单数据结构时,一般都会写一个初始化函数Init,防止野指针访问,但这样很容易让我们忘记去调用!
在C++中,为了避免这种事情,引入了构造函数,在对象实例化时编译器自动调用构造函数进行初始化,所以构造函数是为对象自动初始化而生的!
我们以日期对象为例,对比C与C++的初始化方案:
//C语言实现日期功能 typedef struct C_Date //日期数据结构体 { size_t _year; size_t _month; size_t _day; }C_Date; void InitDate(C_Date* L)//初始化函数 { L->_year = 0; L->_month = 0; L->_day = 0; }
//C++实现日期类 class CPP_Date //日期对象 { public: //默认构造 CPP_Date(size_t year) { _year = year; _month = 1; _day = 1; } //重载实现多种默认构造方式 CPP_Date(size_t year, size_t month, size_t day) { _year = year; _month = month; _day = day; } private: size_t _year; size_t _month; size_t _day; };
可以发现C++中类融入构造函数后只需要实例化对象就能同时完成初始化,非常方便!而且结合C++的缺少参数,函数重载等新特性,可以让初始化丰富多样,增强程序可用性!
对比C与C++,可以发现C++非常贴心,就像汽车中的手动挡与自动挡,但两者在不同场合各有千秋,在程序开发上C++的更胜一筹,在较为底层且需要更细节的程序控制时C语言更胜一筹!不过一般在程序开发中,C与C++可以搭配一起编程!
构造函数的使用与特性
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次(实例化时被编译器调用)。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象成员变量。
构造函数的定义方式:
class Test { public: //构造函数必须公开 Test(参数) {} //构造函数的函数名与类名相同且没有返回值 };
构造函数的特性
- 函数名与类名相同。
- 无返回值(不需要写返回值类型,void也不需要写)。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载,支持多个构造函数,但是默认的构造函数只有一个。
使用构造函数初始化日期对象:
//C++实现日期类 class CPP_Date //日期对象 { public: //默认构造函数只允许出现一种显示,如果定义全缺省就可以代替默认构造了 //CPP_Date()//默认构造函数初始化-功能比较局限 //{ // _year = 0; // _month = 0; // _day = 0; //} //构造函数可以重载实现多种构造方式 CPP_Date(size_t year) { _year = year; _month = 1; _day = 1; } //一般使用全缺省值方式代替默认构造-智能方便 CPP_Date(size_t year = 1970, size_t month = 1, size_t day = 1) { _year = year; _month = month; _day = day; } private: size_t _year; size_t _month; size_t _day; }; int main() { //CPP_Date d(); //注意:这种调用默认构造的方式是错误的,调用默认构造不需要加() CPP_Date d1; CPP_Date d2(2022); CPP_Date d3(2023,3,12); //可以通过调用不同的构造函数实例化多个不同的对象 }
默认构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
那有人可能会问,既然编译器会自动生成构造函数,我们为什么还要去写?
注意:编译器生成的默认构造函数,对内置类型不做处理,对自定义类型会调用该对象的构造函数!
数据类型区分
- 内置类型:int,char,double等。
- 自定义类型:struct,class等,这些自定义类型可以有自己的默认构造函数。
编译器默认构造对内置类型的初始化:
自定义默认构造:
//C++实现日期类 class CPP_Date //日期对象 { public: //无参构造函数 //CPP_Date() //{ // _year = 0; // _month = 0; // _day = 0; //} //全缺省默认构造 CPP_Date(size_t year = 1970, size_t month = 1, size_t day = 1) { _year = year; _month = month; _day = day; } //当存在两种实例化相同的构造方法时,只能存在一种,否则在调用时会出错! //例如 (CPP_Date d;) 编译器无法判断d对象调用的是哪一个构造函数 private: size_t _year; size_t _month; size_t _day; };
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
如果我们定义了构造函数,但是没有定义默认构造,想让编译器去生成时:
class Test { Test() = default; //强制编译器生成默认构造函数 Test(int n) //自定义构造函数 :_n(n) {} private: int _n; }
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
C++11关于默认构造缺陷的补丁
为了解决内置类型无法被编译器生成的默认构造初始化的问题,在C++11中支持内置类型在声明阶段给缺省值,当编译器生成默认构造函数时,使用这些缺省值初始化内置类型。
class CPP_Date //日期对象 { public: private: size_t _year = 1970; //声明阶段赋予缺省值 size_t _month = 1; size_t _day = 1; //注意:类成员变量定义在构造函数的初始化列表,并不是在声明阶段! };
所以,对于内置类型要么自定义默认构造函数,或者在声明时赋予缺省值,对于自定义类型编译器会调用对应的构造函数!
析构函数
我们在写顺序表时,在结束使用时需要调用销毁函数是否内存空间,但是我们可能经常也会忘记释放空间,析构函数就是用来销毁对象和释放空间的!
析构函数是特殊的成员函数,其功能与构造函数相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时(生命周期结束时)会自动调用析构函数,完成对象中资源的清理工作。
析构函数特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型(void也不需要写)。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
对于析构函数,最大的特性是在对象生命周期结束时被自动调用,与构造函数的区别在于不支持重载,也就是说一个对象只能有一个析构函数!
默认析构和自定义析构
如果我们不写析构函数,编译器也会默认生成,但编译器生成的析构函数对内置类型仍然不做处理,对自定义类型会调用对应的析构函数处理!
//析构函数定义 class Test { ~Test() {//释放方法} };
//简易栈对象 typedef int DataType; class Stack { public: Stack(size_t capacity = 3) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; cout << "构造函数初始化栈对象" << endl; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } // 其他方法... ~Stack() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } cout << "析构函数释放栈对象空间" << endl; } private: DataType* _array; int _capacity; int _size; };
对于自定义类型和内置类型,是否存在析构函数进行释放影响都不大,当涉及我们自己动态开辟空间时,就需要使用析构函数释放空间!
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数!对于析构函数,只要是对象中涉及动态内存申请,则需要使用析构函数释放!
拷贝构造函数
拷贝构造也是构造函数的一种,但是参数不同则构成重载,功能和特性与构造函数不同!
问题聚焦
我们在创建对象时,如果需要拷贝一个一模一样的对象,也就是复制一个对象;那么就需要创建一个一模一样的对象后将数据拷贝一份过去。
编译器默认生成的拷贝构造函数只支持浅拷贝,也就是值拷贝,对于内置类型,浅拷贝是没有影响的,但是如果对象中申请了空间,那么该对象中必定有一个指针指向该空间的首地址,浅拷贝只会讲该地址拷贝一份给另一个对象,那么会导致一个严重的问题,就是两个对象申请的空间是同一个地址,在增删查改和析构时拷贝的对象析构函数会对同一片空间进行修改和重复释放一片空间导致异常,最后成为野指针问题!
浅拷贝只是简单的逐字节拷贝,对于对象内部有空间申请的,会发生共用空间重复析构的情况;而深拷贝是在新对象中开辟一块属于自己的新空间,然后将数据逐一拷贝过来,新旧对象之间的数据相互独立!
拷贝构造的定义和特性
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
定义和调用
class Test { //拷贝构造形参的定义方式是唯一的:(const 类型& ) Test(const Test& T) {//拷贝方法} //定义方式 }; int main() { Test t1; Test t2(t1); //调用拷贝构造方式一 Test t3 = t2; //调用拷贝构造方式二 return 0; }
定义须知
- 编译器默认生成的拷贝构造,只是简单拷贝,只能用于非动态内存开辟的空间
- 拷贝构造的参数定义方式是唯一的
- 拷贝构造函数函数名与构造函数相同,不过参数类型为类对象的引用,不加引用则会发生无穷拷贝(因为形参是拷贝而来的,这样形参会陷入无限递归拷贝形参)
使用场景
//日期类示例 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 Date(const Date& d) // 错误写法:编译报错,会引发无穷递归 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); //拷贝构造方式一 Date d3 = d2; //拷贝构造方式二 return 0; }
像日期类这样没有动态内存申请的对象,使用编译器默认生成的拷贝构造进行浅拷贝即可,但是向数据结构需要动态内存申请等相对复杂的对象,就需要自定义拷贝构造实现深拷贝!
拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
说明
- 对于深拷贝,是有一定代价的,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用!
- 默认拷贝构造函数与默认构造函数名相同,当我们只写拷贝而不写构造时,编译器就会报错,因为此时的拷贝会被误以为是默认构造函数也就是说,默认拷贝构造函数存在的前提是默认构造函数已存在。
构造函数小结
构造大家族到这里基本内容就介绍的差不多了!
类型 用途 处理情况 构造函数 初始化对象 不对内置类型作处理,自定义类型调用对应构造函数 析构函数 销毁对象 也不对内置类型作处理,自定义类型调用对应析构函数 拷贝构造函数 拷贝对象 只能对简单内置类型做处理,自定义类型需要自己实现深拷贝
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
运算符重载的引入,主要是为了解决基本运算符功能不足以满足自定义类型需求的情况,例如日期类的加减,以及前后置++和--等,需要自定义对象运算符的功能!
定义方式
返回值类型 operator操作符(参数) { //自定义操作符功能 } // operator 是运算符重载的关键字
特性
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this
对于内置运算符,不能改变其含义
operator操作符 就是函数名
注意这5个运算符不能重载:(1) .* ,(2) :: ,(3)sizeof,(4)?:,(5) .
//日期类 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //为了保证封装性,类成员变量一般是私有,所以运算符重载一般定义在类内部 bool operator==(const Date& d) { //这里相当于 *this 与 d 的 == 比较 return _year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout << (d1 == d2) << endl; //这里相当于调用 d1.operator==(d2) return 0; }
使用说明
- operator 函数中的操作数取决于参数个数
- operator 一般定义在类中,方便访问类成员,当定义在类中时,运算符左边的对象默认是*this
- operator 如果定义在类外,则无法访问类的所有成员,此时要么在类中定义特定函数获取私有成员要么声明为友元函数,但是大部分场景下都没有定义在类中更合适
运算符重载原理
运算符重载的原理与函数重载原理基本相同,也是对函数名修饰。
如果定义在类中在Linux环境下修饰规则为:_ZN4D+类名+运算符英文简称+ERKS_
赋值运算符重载
通过上面的铺垫,我们就要介绍下一个默认成员,那就是赋值重载函数!
class Test { };
赋值重载是将一个对象赋值給另一个对象,与拷贝构造相似,但是拷贝构造是通过一个对象去实例化一个相同的对象,而赋值重载的前提是两个对象已经实例化存在,相互之间再赋值!本质区别在于:一个是对象尚未实例化,另一个是两个对象都已存在!当两个对象都被创建,并发生赋值行为时,才叫做赋值重载!
对于这种自定义类型复制的问题,就会涉及深拷贝和浅拷贝的问题,所以赋值重载是很有必要的!
//日期类 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date& operator=(const Date& d) //与拷贝构造一样能用引用就用引用 { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; //返回赋值的对象的引用(也就是自己) //这里之所以返回自己的引用是因为会发生 d1 = d2 = d3 这样连等的情况 //使用引用可以有效避免拷贝 } private: int _year; int _month; int _day; };
赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
如果赋值运算符重载成全局函数,就没有this指针了,需要给两个参数,而且此时编译器就会报错error C2801: “operator =”必须是非静态成员!
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。默认赋值重载函数:用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
所以赋值重载的使用环境与拷贝构造类似:如果类中未涉及到动态内存申请(资源管理),赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现!
//栈对象 - 猜猜是否会发生与拷贝构造相同的问题 typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; s2 = s1; return 0; }
前后置++和--
对于自定义对象,前后置++和--还是经常会使用到的!
class Test { Test& operator++() {} //前置++ Test operator++(int) {} //后置++ Test& operator--() {} //前置-- Test operator--(int) {} //后置-- };
//以日期类进行介绍 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 前置++:返回+1之后的结果 // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率 Date& operator++() { _day += 1; return *this; } // 后置++: // 注意:后置++是自己+1然后返回+1之前的值 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用 Date operator++(int) { Date temp(*this); _day += 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date d1; d1++; ++d1; return 0; }
原理:前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递(前后置--实现与++相同)。
const修饰this
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
const常被用来修饰引用和指针,使用const修饰可以提高程序的健壮性!
使用场景
- 被(指针)指向对象是常亮或临时变量
- 被引用对象是常亮或临时变量
这些对象必须使用const修饰,避免权限放大的问题!
修饰this指针格式
有小伙伴可能会疑惑,this指针我们不能显示定义,那么如果要const修饰怎么办?
对于this指针的修饰,格式为:
class Test { void Fun() const //将const加在函数参数后即可 { //函数方法 } };
//设想以下日期类代码的运行结果 class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } void Print() const { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2022, 1, 13); d1.Print(); const Date d2(2022, 1, 13); d2.Print(); return 0; }
总之,const修饰this指针可以起到权限平移的作用!
取地址重载和const取地址重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date { public: Date* operator&() //返回对象的地址 { return this; } const Date* operator&() const //返回const修饰对象的地址 { return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
最后
以上就是 类和对象 - 中 的全部内容了,本篇介绍了类的六大默认成员函数,对于构造函数何时用编译器自动生成的,何时自己实现都需要依照情况而定!对于每个成员其规则和细节都很多,需要我们在长期的使用中去牢固的掌握,掌握了这六大成员函数的使用,那么你将对类和对象的掌握又进一步!
本次 <C++类和对象 - 中> 就介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
🌟其他文章阅读推荐🌟
🌹欢迎读者多多浏览多多支持!🌹