二、类的六个默认成员函数

目录
一、类和对象入门
(一)引入类与对象
(二)成员函数的定义
(三)类对象的大小
(四)隐含的this指针
二、类的六个默认成员函数
(一)构造函数
1.带参构造函数
2.自己实现的无参构造函数
3.自己实现的全缺省构造函数
4.编译器自动生成的构造函数
5.注意
(二)析构函数
1.自己写的析构函数
2.编译器自动生成的析构函数
(三)拷贝构造函数
1.自己写的拷贝构造函数
2.编译器自动生成的拷贝构造函数
(四)赋值运算符重载
1.运算符重载
(1)==的重载
(2)>的重载
2.赋值运算符重载
(1)自己写的赋值运算符重载
(2)编译器自动生成的赋值运算符重载
(3)对比拷贝构造和赋值运算符重载
(五)取地址运算符重载
(六)const取地址运算符重载
三、关于类中的一些细节
(一)关于构造函数的一些细节
1.初始化列表
2.关键字
(二)友元
1.友元函数
2.友元类
(三)const成员
1.const修饰引用(常引用问题)
2.const修饰类的 成员函数
(四)成员
1.成员变量
2.成员函数
四、理解封装
一、类和对象入门
面向对象更关注对象与对象之间的关系;面向对象的三大特性:封装、继承、多态 。
(一)引入类与对象
在C语言中,结构体中只能定义变量
在C++中,类(class)和结构体()中可以定义两种:1.成员变量 2.成员函数
注意:一般情况下成员变量都是比较隐私的,都会定义成私有()或保护()
私有和保护就像一把锁:他不让门外面的人进来 , 只可以是门里面的人进行访问
既然C++中都可以用来定义类 , 那么类和结构体之间有什么区别呢?
(1)默认的访问限定符不同:class默认是私有的,默认是共有的 。
(2)对于C++中的:他不仅可以用来定义类 , 他还兼容了C中定义结构体的用法 。
(二)成员函数的定义
1.在类里面定义:编译器可能将成员函数当作内联函数
2.在类里面声明 , 类外面定义:需要使用(::)符号
(三)类对象的大小
如何计算一个类实例化出的对象的大?。考扑愠稍北淞恐停⑶铱悸悄诖娑云牍嬖?
问题一:为什么类对象只保存成员变量,而成员函数放在公共代码段?
答:一个类实例化出N个对象,每个对象的成员变量都可以存储不同的值,但是调用的函数却是
同一个 。如果每个对象都放成员函数,但是这些函数却是一样的 , 那么就会存储重复的东西,浪
费空间 。所以将成员函数存在公共代码段,类实例化出的对象的大小不包含成员函数,只包含成
员变量 。
问题二:没有成员变量的类的大小是1 -> 为什么是1,而不是0?
答:开1个字节不是为了存数据 , 而是占位 。
(四)隐含的this指针
this指针的本质:成员函数的形参(他默认就是成员函数的第一个形参) 。
this指针指向谁?哪个类调用成员函数,this指针就指向调用他的类 。
如果定义class A,那么一个指向A的指针所指向的空间只存着成员变量,没有存成员函数 。
成员函数通过this->_var这种形式就可以访问到成员变量,只是这件事是编译器帮我们完成的 。
二、类的六个默认成员函数
六个默认成员函数特点:自己没写编译器自动生成,自己写了编译器就不会生成了
负责初始化和清理的两个函数:构造函数(负责初始化)、析构函数(负责资源清理工作)
负责拷贝复制的两个函数:拷贝构造函数、赋值运算符重载
两个取地址操作符重载函数:取地址运算符重载,const取地址运算符重载
(一)构造函数
完成对象的初始化工作,而不是对象的创建 。对象创建时自动调用完成初始化工作 。
特征:函数名与类名相同、无返回值、支持重载(有以下四种形式)
下面通过日期类来说明这四种构造函数(关于日期类的定义请关注另一篇博客)
1.带参构造函数
定义
Date(int year, int month, int day){_year = year;_month = month;_day = day;}
调用方式
Date d(2023,10,30);
2.自己实现的无参构造函数
定义
Date(){_year = 0;_month = 1;_day = 1;}
调用方式
Date d2;//调不带参数的构造函数 , 不加括号//Date d2();//不能加括号,要是调带参数的,那就要加括号了
3.自己实现的全缺省构造函数
使用全缺省的目的:将带参构造函数和无参构造函数合二为一 。(最常用)
定义
Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
调用方式
Date d1(2023,11,14);//这种是初始化成自己设置的值Date d2;//这种是初始化成默认值0,1 , 1
4.编译器自动生成的构造函数
如果我们没有显示定义(自己写)构造函数,那么C++编译器会自动生成一个无参的默认构造函数
对于 编译器自动生成的无参默认构造函数(双标)
(1)针对内置类型的成员变量 没有做处理
(2)针对自定义类型的成员变量,调用他的构造函数去初始化
一旦用户显示定义了,编译器将不再生成
5.注意
(1)默认构造函数有3种 特点:不用传参数
自己实现的无参的构造函数②、自己实现的全缺省构造函数③、编译器自动生成的构造函数④
(2)三种默认构造函数不能同时存在,他们三个只能同时存在一个 。
(3)对比两个概念
默认成员函数:指的是最开始说的那六个,自己不写编译器自动生成 。
默认构造函数:说的是(1)中的三种 , 不传参就能调用 。
(4)如果采用声明和定义分离的形式,缺省参数在声明处给就好了,不用在定义处给了 。
(5)C++11中支持一种全新的给出缺省参数的方式
class Date{public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}//c++11支持的在声明时给缺省值(注意这不是初始化)private:int _year = 0;int _month = 1;int _day = 1;};
(二)析构函数
完成对象里面的资源清理工作,不是对象的销毁 。对象生命周期到了以后自动调用完成清理工作
特征:函数名(~类名)、无返回值无参数、不支持重载
问题:构造和析构的顺序?先构造的后析构 。原因:因为对象是存在栈上的 , 满足后进先出 。
Date d1;Date d2;
先构造d1,后构造d2;先析构d2,后析构d1 。
1.自己写的析构函数
以栈类为例:注意??函数名(~类)
class Stack{public://构造函数Stack(int n = 10){_arr = (int*)malloc(sizeof(int)*n);_size = 0;_capacity = n;}//析构函数~Stack(){free(_arr);_arr = nullptr;_size = _capacity = 0;}private:int* _arr;size_t _size;size_t _capacity;};
2.编译器自动生成的析构函数
如果我们没有显示定义(自己写)析构函数 , 那么C++编译器会自动生成一个析构函数
对于 编译器自动生成的析构函数(双标)
(1)针对内置类型的成员变量,没有做处理 。
(2)针对自定义类型的成员变量,调用他的析构函数 。
一旦用户显示定义了,编译器将不再生成
(三)拷贝构造函数 1.自己写的拷贝构造函数
首先说明:1.拷贝构造函数是构造函数的一个重载形式 2.拷贝构造函数只有一个引用参数
解释:为什么拷贝构造函数只有一个引用参数,而且传值方式会引发无穷递归?
Date d(d1);
将d1传给d是一次拷贝构造(函数传参),但这时拷贝构造函数还没有写呢 。也就是说在拷贝构
造函数还不存在的情况下就要调用拷贝构造函数了,所以就会引发无穷递归 。
拷贝构造函数的定义(以日期类为例)
解释const的作用:保护d为别名的那块空间的内容不被修改 。
Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}
拷贝构造函数的调用(两种方式)
Date d(d1);//方式一Date d = d2;//方式二
2.编译器自动生成的拷贝构造函数
编译器生成的拷贝构造,会完成按字节的值拷贝(浅拷贝) 。
对于Date类这种,完成浅拷贝即可,所以编译器生成的足够用 。但是Stack类这种 , 必须自己去
实现深拷贝 。
(四)赋值运算符重载
要了解赋值运算符重载,我们可以先学习运算符重载
1.运算符重载
对于内置类型(int long···)可以直接使用== !=这种运算符 。
对于自定义类型,我们不可以直接使用自带的运算符;要想直接使用,就要用到运算符重载 。
有关运算符重载的几点说明
①对于运算符重载,有一个关键字 。
②.*::?:.这五个运算符不能重载 。
下面用日期类来演示具体用法 。
(1)==的重载
第一种写法:将运算符重载函数写在类外面(这样写要将成员变量改成)
注意??运算符有几个操作数,重载的函数就有几个参数 。
注意??自定义类型是不能用运算符的,要用就得实现重载函数,自定义类型用的时候等价于调
用这个重载函数 。
bool operator==(const Date& d1, const Date& d2){return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;}//使用cout << (d1 == d2) << endl;
第二种写法:将运算符重载函数写在类里面(这样写要将成员变量可以是私有)
这种写法更常用 , 因为我们一般都需要成员变量是私有的 。
//这里实际上有两个参数:一个是this指针,另一个是操作数bool operator==(const Date& d)//bool operator==(Date* this,const Date& d){return _year == d._year && _month == d._month && _day == d._day;}//使用cout << (d1 == d2) << endl;
(2)>的重载
bool 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;}//其他情况都是falsereturn false;}//使用cout << (d1 > d2) << endl;
这里只介绍==和>的重载,因为这足以说明运算符重载的写法 。
xxx以日期类为例,介绍了各种运算符重载 。
2.赋值运算符重载 (1)自己写的赋值运算符重载
//这里返回值类型为Date是为了实现连续赋值//用引用返回是因为返回值*this出了函数作用域还存在//引用返回还可以减少拷贝构造,提高效率Date& operator =(const Date& d){if(this != &d)//针对自己给自己赋值的检查判断//如果出现自己给自己赋值 , 可以不走下面的步骤 , 提高效率{_year = d._year;_month = d._month;_day = d._day;}return *this;}
(2)编译器自动生成的赋值运算符重载
编译器生成的= , 会完成按字节的值拷贝(浅拷贝) 。
对于Date类这种 , 完成浅拷贝即可 , 所以编译器生成的足够用 。但是Stack类这种,必须自己去
实现深拷贝 。
(3)对比拷贝构造和赋值运算符重载
①调用形式上的差别
//拷贝构造Date d(d1);Date d = d1;//赋值运算符重载d2 = d1;
②拷贝构造函数和= 都是默认成员函数,我们不现实时,编译器会帮我们实现一份 。
我们不现实时,编译器生成的拷贝构造和= 。会完成按字节的值拷贝(浅拷贝) 。
也就是说有些类,我们是不需要去实现拷贝构造和=,因为编译器默认生成的就可以用
比如:Date类就是这样,但是Stack类就不可以(因为需要深拷贝)
(五)取地址运算符重载
一般不用自己写,编译器自动生成的足够我们使用 。
Date* operator &(){cout << "Date* operator &()" << endl;//用于测试是否被调用return this;}//调用Date d1;cout << &d1 << endl;
(六)const取地址运算符重载
一般不用自己写,编译器自动生成的足够我们使用 。
const Date* operator &() const{cout << "const Date* operator &() const" << endl;//用于测试是否被调用return this;}//调用const Date d2;cout << &d2 << endl;
三、关于类中的一些细节 (一)关于构造函数的一些细节 1.初始化列表
我们之前一直这样写构造函数,这种构造函数调用之后,对象中就已经有了一个初始值,但是这
个过程不能称作对象的初始化 。这样写只能叫做赋初值,而不能叫做初始化 。
因为初始化只能初始化一次,而在构造函数内可以多次赋值(显然第一次赋值就是赋初值)
Date(int year = 0, int month = 1, int day = 1){//函数体内赋值_year = year;_month = month;_day = day;}
写成初始化列表就是初始化了,具体语法格式如下
Date(int year = 0, int month = 1, int day = 1):_year(year),_month(month),_day(day){}
现在就有疑问了,那我保证在函数体内只赋值一次不就好了?为什么非要搞出一个初始化列表?
解释:①引用成员变量、const成员变量、没有默认构造函数的自定义类型的成员变量
上述三种成员变量必须进行初始化,所以就一定需要初始化列表?。。?
②建议优先使用初始化列表,因为初始化的效率比赋值的效率高 。
注意??声明和初始化列表的顺序要保持一致,这样可以减少出错!
//最好要顺序一致,如下所示class Test{public:Test(int a1, int a2):_a1(a1),_a2(a2){}private:int _a1;int _a2;};//尽量不要这样class Test{public:Test(int a1, int a2):_a2(a2),_a1(a1){}private:int _a1;int _a2;};
2.关键字
要想学习关键字,就要先辨别这三种写法的区别 。
(1)单参数隐士类型转换【第三种写法才是】
class Date{public:Date(int year):_year(year){}private:int _year;int _month;int _day;};//三种写法Date d1(1);//构造函数Date d2 = d1;//拷贝构造Date d3 = 3;//单参数隐士类型的转换 构造出tmp(3)+在用tmp拷贝构造d3(tmp)
(2)C++11支持的多参数隐士类型转换【第三种写法才是】
class Date{public:Date(int year ,int month ,int day):_year(year),_month(month),_day(day){}private:int _year;int _month;int _day;};//三种写法Date d1(2023,11,18);//构造函数Date d2 = d1;//拷贝构造Date d3 = {2023,11,19};//多参数隐士类型的转换
我们看见第三种写法很奇怪,要是不想出现这种隐士类型转换,就在构造函数前加上
也就是说在构造函数前加上,就不可以出现第三种写法了 。
class Date{public:explicit Date(int year):_year(year){}private:int _year;int _month;int _day;};Date d1(1);//构造函数Date d2 = d1;//拷贝构造//Date d3 = 3;//隐士类型转换(加上explicit就报错了)
(二)友元
友元()是一种访问私有和保护的方式:正常情况下我们在类外面访问不了私有和保护,
但是如果我们成为朋友,那我就可以访问你的私有和保护了 。
1.友元函数
(1)相关知识
①友元函数可以访问类的私有和保护成员,但不是类的成员函数 。
②友元函数不能用const修饰 。
③友元函数可以在类的任何地方声明,不受访问限定符的限制 。
④一个函数可以是多个类的友元函数 。
(2)应用:重载>
不使用友元函数同样可以重载> , 那我们为什么要使用友元呢?
不使用友元,在类里面重载>,使用的时候只能这样:d1 非const
class Test{public:void f1() const{}void f2(){}};const Test t2;t2.f1();//const对象可以访问const成员函数//t2.f2();//const对象不可以访问非const成员函数
②非const对象可以调用const成员函数 。权限缩?。悍莄onst->const
class Test{public:void f1() const{}void f2(){}};Test t1;t1.f1();//非const对象可以调用const成员函数函数t1.f2();//非const对象可以调用非const成员函数函数
(2)成员函数调用 const成员函数
①const成员函数不可以调用非const成员函数 。权限放大:const->非const
//f3 f4属于放大行为 不行void f3()//void f1 (date* this){}void f4()const//void f4(const date* this){f3();//this->f3(this)}
②非const成员函数可以调用const成员函数 。权限缩?。悍莄onst->const
//f1 f2属于缩小行为 可以void f1()//void f1 (date* this){f2();//this->f2(this)}void f2() const{}
(四)成员 1.成员变量
成员变量 不存在对象中,而是存在静态区 。他属于这个类的所有对象,也属于这个类本身 。
2.成员函数
(1)成员函数,没有this指针,不使用对象就可以调用 。
-> 类名::func() 【??这种方式只能调用静态成员函数】
(2)成员函数,不能访问 类中的非静态成员(成员变量+成员函数)
但是非静态成员函数可以访问静态成员(成员变量+成员函数)
①静态成员函数不能访问非静态成员变量 和 非静态成员函数
总结:静态成员函数 只能访问静态的(成员变量+成员函数)
class Test{public:void f1(){}static void f2(){//f1();//静态成员函数不能访问非静态成员函数//_a = 10;//静态成员函数不能访问非静态成员变量}private:int _a;static int _n;};
②非静态成员函数可以访问静态成员变量 和 静态成员函数
总结:非静态成员函数 静态和非静态(成员变量+成员函数)都能访问
class Test{public:static void f3(){}void f4(){}void f5(){f3();//非静态成员函数可以访问静态成员函数_n = 100;//非静态成员函数可以访问静态成员变量f4();//非静态成员函数可以访问非静态成员函数_a = 10;//非静态成员函数可以访问非静态成员变量}private:int _a;static int _n;};
四、理解封装
1.数据和方法定义到一起 。
2.把想给你看到的数据给你看,不想给你看到的封装起来 。(通过三种访问限定符实现)
【二、类的六个默认成员函数】3.一般成员变量为私有,成员函数为公有 。我们只能通过接口函数去改变数据 。