0.前言
您好,这里是limou3434的一篇有关C++类的博文,感兴趣的话您可以看看我的其他博文系列。
有关类的知识,我分成了两份(上和下部分)来讲解,涉及到构造、析构、友元、拷贝复制等内容。
本篇为上半部分,主要得内容在于类的成员函数。
希望您能学得愉快。
1.面向过程和对象
1.1.面向过程
C语言就是面向过程的语言,解决问题时,关注的是过程,通过函数的调用逐步解决问题。
1.2.面向对象
C++语言,是面向对象和面向过程混编语言,解决问题时,关注的是对象,将一件事情拆分为不同的对象,靠对象交互完成。
我想煮饭,于是需要对象:我、电饭煲、菜,“我”把“菜”放入“电饭锅”,不需要了解我是通过哪一块肌肉放入菜,不需要了解菜是具有什么分子结构,不需要了解电饭煲的工作原理,煮饭都是靠“我”、“菜”、“电饭煲”三者交互完成的。
关于“面向对象”概念的理解,您可暂时不必理会,等您学好C++的语法过后再来理解也不迟。
2.类的引入
2.1.C语言结构体和C++类的区别
C语言的结构体只能定义变量,而结构体不仅可以定义变量,也可以定义函数,但是对于结构体在内部加入函数的方式,C++更喜欢用class来代替struct。
2.2.类的概念和使用
下面我们使用struct来定义一个基础的C++类/结构体
//类和对象的使用
#include <iostream>
#include <cstdio>
using namespace std;
struct str
{
int a;
int b;
int Xadd(int x, int y)//C++的结构体是升级版本的结构体
{
return a * x + b * y;
}
};
int main()
{
str X;
X.a = 2;
X.b = 5;
printf("%d\n", X.Xadd(1, 2));
return 0;
}
//而且C++的结构体名可以省略struct的写法
但是在C++里最为标准的类的写法是使用class,上面的处理只是为了兼容C语言。
//类和对象的使用
#include <iostream>
#include <cstdio>
using namespace std;
class str
{
int a;
int b;
int Xadd(int x, int y)//C++的结构体是升级版本的结构体
{
return a * x + b * y;
}
};
int main()
{
str X;
X.a = 2;//出错
X.b = 5;//出错
printf("%d\n", X.Xadd(1, 2));
return 0;
}
- class是定义类的关键字,后面跟着类的名字,即“类名”,{}中为类的成员
- 类中的变量称为类的属性(成员变量)
- 类中的函数称为类的方法(成员函数)
但是这么写为什么没有办法运行呢?这是因为类还有三个限定符,分别是:public(公有)、protected(保护)、private(私有),在没有使用限定符的情况下,类默认是私有的,但是结构体默认是公有的(这是为了兼容C)。
2.3.类的定义方式
2.3.1.声明和定义全部放在类中
class 类名
{
public:
//某些方法(包括声明和定义)
public:
//某些属性
}
需要注意的是,如果成员函数在类中直接定义,编译器可能当成内联函数处理。因此可以利用这一点,较小的函数若要做内联,则直接写到类内部
2.3.2.类声明放在含类的头文件,成员函数定义放在源文件
//类头文件class.h中
class 类名
{
public:
//某些方法基本信息(函数声明)
public:
//某些属性
}
//类源文件class.c中
#include "class.h"
函数返回值 类名::函数名字()
{
//某些方法定义(函数方法)
}
- 一般来说后者会比较规范一些,但是为了更加好演示代码,后续的文章都采用第一种,正式工作必须要用第二种
- 而且可以尝试区分好方法名字和属性名字,比如在属性面前加上下划线、函数名字采用大驼峰、变量名字采用小驼峰
2.4.类的访问限定符及封装
2.4.1.C++实现封装的方式
用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部用户使用。
- public(公有),public修饰的成员在类外可以直接被访问
- protected(保护),protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- private(私有),protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到“}”即类结束
- class的默认访问权限为private,struct为public(因为struct要兼容C,在C语言中结构体是可以被直接访问的)
- protected和private在后面讲到继承的时候才能进一步做区分
而访问限定符的作用范围从该访问限定符出现的位置开始直到下一个访问限定符出现为止。
//类和对象的使用
#include <iostream>
#include <cstdio>
using namespace std;
class str
{
private:
int a;
int b;
public:
int Xadd(int x, int y)//C++的结构体是升级版本的结构体
{
a = 2;
b = 5;
return a * x + b * y;
}
};
int main()
{
str X;
printf("%d\n", X.Xadd(1, 2));
return 0;
}
注意访问限定符只有在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
2.4.2.封装的概念
面向对象的三大特性:封装、继承、多态(这三个是最重要的,但是面向对象还有其他的特性,例如“抽象”)。其中封装本质是以一种管理,封装将数据和操作数据的方法有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装让用户更加方便使用类。而C++通过访问限定符来控制隐藏对象的细节,控制哪些方法可以在类外部直接使用。
2.5.类的作用域
类的出现赋予了C++一个新得作用域:类的作用域,即“类域”
- 在类的里面,类的所有成员都在类的作用域中
- 在类外面定义成员时,需要使用“::”作用域操作符来指明
//类和对象的使用
#include <iostream>
#include <cstdio>
using namespace std;
class str
{
private:
int a;
int b;
public:
int Xadd(int x, int y)//C++的结构体是升级版本的结构体
{
a = 2;
b = 5;
return a * x + b * y;
}
};
int main()
{
str X;
printf("%d\n", X.Xadd(1, 2));
return 0;
}
需要注意的是,哪怕有访问限定符,只要使用的是同一个类。就不受限定符的影响
#include <iostream>
using namespace std;
class A
{
public:
void f1(int x)
{
_a = x;
}
void f2(A x)
{
cout << x._a << endl;//这里没有报错,说明同类的使用是不会被访问限定符限定的
}
void Prin(void)
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A x;
A y;
x.f1(2);
y.f1(3);
x.Prin();
y.Prin();
y.f2(x);
return 0;
}
2.6.类的实例化
给类创建对象的过程,称为类的实例化
- 类是对对象进行描述,是对象的一个模板,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它(也就是说类的成员是一种声明,而不是定义)
- 类可以实例化(也就是类的定义)出多个对象来,实例化出的对象会占有实际的物理空间,也就是定义
//错误做法
#include <iostream>
using namespace std;
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
int main()
{
Person._age = 10; // 编译失败:error C2059: 语法错误:“.”,因为没有对
return 0;
}
//正确做法
#include <iostream>
using namespace std;
class Person
{
public:
void PrintPersonInfo();
public:
char* _name;
char* _sex;
int _age;
};
void Person::PrintPersonInfo()
{
cout << _name << " " << _sex << " " << _age << endl;
}
int main()
{
Person man;
man._name = (char*)"limou3434";//由于C++类型检查比较严格,故加上强制转化
man._age = 10;
man._sex = (char*)"男";//由于C++类型检查比较严格,故加上强制转化
man.PrintPersonInfo();
return 0;
}
2.7.求类对象的大小
// 类中既有成员变量,又有成员函数
class A1
{
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
//结果为4 1 1
- 实际就是该类中“成员变量”之和,不过要注意内存对齐!这点和结构体内存对齐是一样的……(类在计算大小中是不算上成员函数的)
- 对象存储方法一:存储“类函数+类成员变量”假设大小含有成员函数,那么每次都实例化一个类时会重复多出函数的栈帧空间,浪费
- 对象存储方法二:存储“类函数表地址+类成员变量”,其中类函数表地址指向一个类函数表(这在虚表和多态被采用)
- 对象存储方法三:只保存成员变量,成员函数存放在公共的代码段。编译链接的时候就会根据函数名去公共代码区找到函数的地址(类似“a.Function()”时,会使用call替换,即call函数地址)
class A
{
public:
int i;
int j;
public:
void Print(void);
};
void A::Print(void)
{
printf("Helow word!\n");
}
int main()
{
A* ptr = nullptr;
ptr->Print();//正常运行,因为这里不是真正的解引用,原因是function()函数没有真正放在类中,而是放在了公共代码区,这里只是单纯的调用罢了
return 0;
}
- 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象,只做占位使用,不存储实际数据。
3.this指针
3.1.隐藏this指针
class Data
{
public:
void Init(int year, int month, int day)//3.但是函数是这么找到实例化后变量里的_year呢?其实就是在这里的参数列表隐藏了一个this指针
{
_year = year;//2.而这里的year值并不是赋给了类里的_year,而是实例化后的d里的_year
_month = month;
_day = day;
}
void Print(void);
private:
int _year;//1.没有经过实例化的类,因此这里的_year只是声明
int _month;
int _day;
};
void Data::Print(void)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int main()
{
Data d;
d.Init(2023, 6, 8);
d.Print();
return 0;
}
3.2.显示this指针
但是不能手动加上(即:不能显示传递和显示接受),这是编译器在做的事情,编译器会帮您自动加上。但是有一个例外,您可以在函数内部直接使用this指针,这点C++没做限制。
class Data
{
public:
void Init(Date* const this, int year, int month, int day)//3.但是函数是这么找到实例化后变量里的_year呢?其实就是在这里的参数列表隐藏了一个this指针
{
this->_year = year;//2.而这里的year值并不是赋给了类里的_year,而是实例化后的d里的_year
this->_month = month;
this->_day = day;
}
void Print(Date* const this);
private:
int _year;//1.没有经过实例化的类,因此这里的_year只是声明
int _month;
int _day;
};
void Data::Print(Data* const this)
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
int main()
{
Data d;
d.Init(&d1, 2023, 6, 8);
d.Print(&d1);
return 0;
}
3.3.this指针的特性
class Cl
{
public:
int x;
int y;
public:
void Print()//3.程序来到这里后,这里的函数头等价于“Print(Cl* const this)”
{
cout << x << endl;//1.程序在这里发生奔溃
//4.上面这一句展开来写就是“cout << this->x << endl;”这个时候就发生空指针解引用了,程序就会发生奔溃
}
};
int main()
{
Cl* C = nullptr;//2.原因是由于这里是空指针
C->Print();//3.这里展开写就是“C->Print(C);”
return 0;
}
3.4.this指针的存储位置
那么this指针应该存储到哪里去呢?存储道栈的区域,因为this本质还是一个形参,存储在栈上。但是这是不一定的,有的编译器会进行优化,比如:VS2022会采用寄存器去优化代码,这点可以查看反汇编看看即可(这里粗略了解一下就可以)
class Cl
{
public:
int x;
int y;
public:
void Print()
{
00007FF7B09420C0 mov qword ptr [rsp+8],rcx
00007FF7B09420C5 push rbp
00007FF7B09420C6 push rdi
00007FF7B09420C7 sub rsp,0E8h
00007FF7B09420CE lea rbp,[rsp+20h]
00007FF7B09420D3 lea rcx,[__E91ADDBE_main@cpp (07FF7B09540E3h)]
00007FF7B09420DA call __CheckForDebuggerJustMyCode (07FF7B09413FCh)
cout << x << endl;
00007FF7B09420DF mov rax,qword ptr [this]
00007FF7B09420E6 mov edx,dword ptr [rax]
00007FF7B09420E8 mov rcx,qword ptr [__imp_std::cout (07FF7B0952190h)]
00007FF7B09420EF call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7B0952168h)]
00007FF7B09420F5 lea rdx,[std::endl<char,std::char_traits<char> > (07FF7B094103Ch)]
00007FF7B09420FC mov rcx,rax
00007FF7B09420FF call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7B0952170h)]
}
00007FF7B0942105 lea rsp,[rbp+0C8h]
00007FF7B094210C pop rdi
00007FF7B094210D pop rbp
00007FF7B094210E ret
};
int main()
{
00007FF7B0942150 push rbp
00007FF7B0942152 push rdi
00007FF7B0942153 sub rsp,108h
00007FF7B094215A lea rbp,[rsp+20h]
00007FF7B094215F lea rdi,[rsp+20h]
00007FF7B0942164 mov ecx,0Ah
00007FF7B0942169 mov eax,0CCCCCCCCh
00007FF7B094216E rep stos dword ptr [rdi]
00007FF7B0942170 mov rax,qword ptr [__security_cookie (07FF7B094E018h)]
00007FF7B0942177 xor rax,rbp
00007FF7B094217A mov qword ptr [rbp+0D8h],rax
00007FF7B0942181 lea rcx,[__E91ADDBE_main@cpp (07FF7B09540E3h)]
00007FF7B0942188 call __CheckForDebuggerJustMyCode (07FF7B09413FCh)
Cl C;
C.x = C.y = 10;
00007FF7B094218D mov dword ptr [rbp+0Ch],0Ah
00007FF7B0942194 mov eax,dword ptr [rbp+0Ch]
00007FF7B0942197 mov dword ptr [C],eax
C.Print();
00007FF7B094219A lea rcx,[C]
00007FF7B094219E call Cl::Print (07FF7B0941474h)
return 0;
00007FF7B09421A3 xor eax,eax
}
00007FF7B09421A5 mov edi,eax
00007FF7B09421A7 lea rcx,[rbp-20h]
00007FF7B09421AB lea rdx,[string "%d\n"+58h (07FF7B094AC80h)]
00007FF7B09421B2 call _RTC_CheckStackVars (07FF7B0941384h)
00007FF7B09421B7 mov eax,edi
00007FF7B09421B9 mov rcx,qword ptr [rbp+0D8h]
00007FF7B09421C0 xor rcx,rbp
00007FF7B09421C3 call __security_check_cookie (07FF7B0941208h)
00007FF7B09421C8 lea rsp,[rbp+0E8h]
00007FF7B09421CF pop rdi
00007FF7B09421D0 pop rbp
00007FF7B09421D1 ret
这里还可以对比下C和C++分别实现的Stack,不过这点可以以后再说。
4.类的默认成员函数
实际上,一个空类并不是真的“空”,编译器还会自动生成6个默认成员函数(默认成员函数是用户没有显式实现的。编译器会自动生成的成员函数称为默认/缺省成员函数)
class ClassName{
//某些成员
};
- 初始化和清理
- 构造函数完成初始化
- 析构函数完成清理
- 拷贝复制
- 拷贝构造函数使用同类对象初始化创建对象
- 运算符重载
- 赋值重载函数主要是把一个对象赋值给另外一个对象
- 主要是普通对象和const对象取地址,这两个很少会自己实现,目前还不太重要(有机会再细谈)
4.1.初始化和清理
4.1.1.构造函数
构造函数可以帮助我们初始化,而不至于忘记初始化而得到随机值或者崩溃。构造函数是类里特殊的成员函数,虽然叫“构造”,但是它的工作不是开辟空间,而是进行初始化工作(构造函数这个名字其实起得不够好,容易引发误会)。由于这个函数很特殊,所以在理解的时候可以特殊化理解。
构造函数的特征如下:
- 函数名和类相同
- 无返回值,也不用写void(这里的没有返回值不是返回空(void),而是根本就没有返回值这一说法,构造函数只需要做到初始化即可,因此我们说它是个特殊的函数)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
//没有构造函数的情况
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//创建一个d1结构体变量
d1.Init(2022, 7, 5);
d1.Print();
Date d2;//创建一个d2结构体变量
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
//使用构造函数后
class Date
{
public:
Date()
{
_year = 2023;
_month = 6;
_day = 8;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//创建一个d1结构体变量
d1.Print();//自动调用构造函数
return 0;
}
因此光是这个自动初始化,在C++中就能省略很多代码,还可以使用
//全缺省的构造函数(更加好用)
class Date
{
public:
Date(int year = 2000, 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()
{
Date d1(2022);
d1.Print();
Date d2(2023, 7);
d2.Print();
Date d3(2002, 9, 8);
d3.Print();
return 0;
}
另外,如果没有显示定义构造函数,编译器就会自动生成一个无参的构造函数,如果有显示定义构造函数,编译器则不会自动生成。但是如果编译器自动生成的构造函数,真的会帮助我们初始化吗?因此答案是:在默认生成的构造函数中,对内置类型成员(例如char、int、以及所有的指针类型等)不做初始化处理,但是自定义类型成员(例如类类型)回去调用自定义类型成员变量自己的默认构造函数(但是这种构造函数必须是全缺省或无参的)。这是C++早期设计的一个缺陷,当然有的新编译器对这些进行了处理。
但是在C++的时候打了一个补丁,可以通过给类成员缺省值。
//默认构造函数对内置类型不起作用
class Cl
{
public:
Cl(int x = 1)
{
_x = x;
}
public:
int _x;
};
class Date
{
public:
//Date(int year = 2000, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << _a1._x << " " << _a2._x << endl;
}
private:
Cl _a1;
Cl _a2;
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();//观察到内置的_a1自动进行了初始化,但是其他的
return 0;
}
//默认构造函数对内置类型起作用
class Date
{
public:
//Date(int year = 2000, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;//注意这是缺省值,交给默认构造函数处理的,这样内置类型也可以做到通过构造函数来初始化
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
总结起来,实际上在C++中默认构造函数有三类:
- 我们没写,编译器自动生成的默认构造函数
- 我们自己写的,全缺省的默认构造函数
- 我们自己写的,无参的默认构造函数
另外,这三钟默认构造函数不可能同时存在。
4.1.2.析构函数
析构函数和构造函数功能相反。析构函数不是完成对对象本身的销毁,局部对象销毁的工作由编译器完成。那析构函数究竟做什么呢?析构函数在对象销毁后会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特征:
- 析构函数的书写方法是在类名前面加上“~”,意思就是和构造函数功能相反
- 无参数无返回值
- 一个类只能有一个析构函数,若未显示定义,系统就会自动生成默认的析构函数。(即:析构函数不能重载)
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 注意局部对象中析构的顺序:先定义的后析构,后定义的先析构。如果是静态对象或者全局对象,则情况会再复杂一点,总体来看就是全局和静态的都要比局部的对象慢使用析构函数,而全局和静态的对象的析构顺序取决于两者的先后顺序
#include <iostream>
#include <cstdio>
using namespace std;
class Data1
{
public:
//1.构造函数
Data1(int x = 1, int y = 1)
{
_x = x;
_y = y;
cout << "Data()" << endl;
}
//2.析构函数
~Data1()
{
cout << "~Data1()" << endl;
//这个析构函数是没啥作用的,这里是演示确实调用了析构函数
}
//3.其他接口
void Print(void);
int Add(void);
public:
int _x;
int _y;
};
void test(void)
{
Data1 C;
C.Print();
printf("%d\n", C.Add());
}
int main()
{
test();
return 0;
}
void Data1::Print(void)
{
printf("%d %d\n", _x, _y);
}
int Data1::Add(void)
{
return _x + _y;
}
那么析构函数在什么时候才更加具有作用呢?在调用堆空间的时候析构函数就有很大的作用
#include <iostream>
#include <cstdio>
using namespace std;
class Data1
{
public:
//1.构造函数
Data1(int x = 1, int y = 1)
{
_x = x;
_y = y;
cout << "Data()" << endl;
}
//2.析构函数
~Data1()
{
cout << "~Data1()" << endl;
//这个析构函数是没啥作用的,这里是演示确实调用了析构函数
}
//3.其他接口
void Print(void);
int Add(void);
public:
int _x;
int _y;
};
void test(void)
{
Data1 C;
C.Print();
printf("%d\n", C.Add());
}
int main()
{
test();
return 0;
}
void Data1::Print(void)
{
printf("%d %d\n", _x, _y);
}
int Data1::Add(void)
{
return _x + _y;
}
但是如果我们没有显式的写出析构函数,那么编译器自己执行的默认析构函数又有什么作用呢?和构造函数类似,隐式的析构函数:对内置类型不做处理,但是会处理自定义类型。
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Data1
{
public:
//1.构造函数
Data1(int x = 1, int y = 1)
{
_x = x;
_y = y;
cout << "Data()" << endl;
int* cache = (int*)malloc(sizeof(int) * 5);
if (!cache) exit(-1);
_arr = cache;
}
//2.析构函数
~Data1()
{
cout << "~Data1()" << endl;
free(_arr);
_arr = nullptr;
_x = _y = 0;
}
//3.其他接口
void Print(void);
int Add(void);
void ForPrint(void);
public:
int _x;
int _y;
int* _arr;
};
class Data2
{
public:
Data2()
{
int* cache = (int*)malloc(sizeof(int) * 1);
if (!cache) exit(-1);
_d = cache;
}
public:
Data1 _a;
Data1 _b;
int _c = 1;
int* _d;
};
void test(void)
{
Data2 D;
cout << D._a._arr;//这个arr就会被默认的析构函数清理,因为_a的数据类型书自定义类型,拥有自己的析构函数,默认的析构函数去调用了这个自定义的析构函数
cout << D._d;//这里_d就不会被默认的析构函数释放掉,因为其是内置类型,因此造成了内存泄露
}
int main()
{
test();
//这里就体现了析构函数的价值,使用析构函数就可以在这里避免内存泄露
return 0;
}
void Data1::Print(void)
{
printf("%d %d\n", _x, _y);
}
int Data1::Add(void)
{
return _x + _y;
}
void Data1::ForPrint(void)
{
for (int i = 0; i < 5; i++)
{
_arr[i] = i * i;
printf("%d ", _arr[i]);
}
printf("\n");
}
注意这和Java的垃圾回收机制是不一样的,不能混为一谈!利用析构函数在书写某些数据结构的时候就很方便。
下面演示构造函数和析构函数的调用顺序问题
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class A
{
public:
A(int x)
{
_a = x;
cout << "A():" << "aa" << _a << endl;
}
~A()
{
cout << "~A():" << "aa" << _a << endl;
}
private:
int _a;
};
A aa0(0);
void f(void)
{
static A aa1(1);
A aa2(2);
A aa3(3);
static A aa4(4);
}
int main()
{
f();
//构造aa0
//构造aa1
//构造aa2
//构造aa3
//构造aa4
//析构aa3
//析构aa2
f();
//构造aa2
//构造aa3
//析构aa3
//析构aa2
return 0;
}
//析构aa4
//析构aa1
//析构aa0
4.2.拷贝复制
在创建对象时,可否创建一个与已存在对象相同的的新对象呢?这个时候就可以用到拷贝构造函数,拷贝构造函数的特征有:
- 只有单个形参,该形参是对本类型对象的“引用”(一般常用const修饰,使用传值方式编译器会直接报错,因为会引发无穷递归调用,当然用指针也不错,但是形式上怪怪的),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数时构造函数的一个重载形式
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Data1
{
public:
//1.构造函数
Data1(int x = 1, int y = 1)
{
_x = x;
_y = y;
cout << "Data()" << endl;
int* cache = (int*)malloc(sizeof(int) * 4);
if (!cache) exit(-1);
_arr = cache;
}
//2.析构函数
~Data1()
{
cout << "~Data1()" << endl;
free(_arr);
_arr = nullptr;
_x = _y = 0;
}
//3.拷贝构造函数
Data1(const Data1& d)
{
_x = d._x;
_y = d._y;
int* cache = (int*)malloc(sizeof(int) * 4);
if (!cache) exit(-1);
memcpy(cache, d._arr, sizeof(int) * 4//另外,拷贝构造函数还会涉及到深拷贝和浅拷贝的知识,不过这个后面再说了
_arr = cache;
}
//4.其他接口
void Print(void);
int Add(void);
void ForPrint(void);
public:
int _x;
int _y;
int* _arr;
};
void test(void)
{
Data1 E(2, 3);
E.Print();
printf("%d\n", E.Add());
//方法一:兼容C的一种拷贝方法
Data1 F = E;
E.Print();
//方法二:使用类的拷贝构造函数
Data1 G(E);
G.Print();
}
int main()
{
test();
//这里就体现了析构函数的价值,使用析构函数就可以在这里避免内存泄露
return 0;
}
void Data1::Print(void)
{
printf("%d %d\n", _x, _y);
}
int Data1::Add(void)
{
return _x + _y;
}
void Data1::ForPrint(void)
{
for (int i = 0; i < 4; i++)
{
_arr[i] = i * i;
printf("%d ", _arr[i]);
}
printf("\n");
}
那么构造拷贝函数如果是编译器默认生成的会有什么作用呢?实际上,编译器生成的默认拷贝构造函数会对“内置类型”直接按照字节的方式直接拷贝(是比较粗暴的浅拷贝),而“自定义类型”则回去调用其自己的拷贝构造函数完成拷贝。
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Data1
{
public:
//1.构造函数
Data1(int x = 1, int y = 1)
{
_x = x;
_y = y;
cout << "Data()" << endl;
int* cache = (int*)malloc(sizeof(int) * 4);
if (!cache) exit(-1);
_arr = cache;
}
//2.析构函数
~Data1()
{
cout << "~Data1()" << endl;
free(_arr);
_arr = nullptr;
_x = _y = 0;
}
//3.拷贝构造函数
Data1(const Data1& d)
{
_x = d._x;
_y = d._y;
int* cache = (int*)malloc(sizeof(int) * 4);
if (!cache) exit(-1);
memcpy(cache, d._arr, sizeof(int) * 4);
_arr = cache;
}
//4.其他接口
void Print(void);
int Add(void);
void ForPrint(void);
public:
int _x;
int _y;
int* _arr;
};
class Data2
{
public:
void Print(void)
{
cout << _data1._x << " " << _data1._y << endl;
cout << _data3 << " " << _data4 << endl;
}
void Fun(int x, int y)
{
_data1._x = x;
_data1._y = y;
_data3 = 100000;
_data4 = 'X';
}
public:
Data1 _data1;
Data1 _data2;
int _data3 = 0;
char _data4 = 'a';
};
void test(void)
{
Data2 H;
H.Print();
cout << endl;
H.Fun(100000, 100000);
H.Print();
cout << endl;
Data2 I(H);
//这一步拷贝,使用的是编译器自动给出的默认拷贝构造函数。
//对于内置类型会自己进行拷贝,没必要我们写出来
//对于自定义类型则会回去调用自定义类型的拷贝函数
I.Print();
}
int main()
{
test();
return 0;
}
void Data1::Print(void)
{
printf("%d %d\n", _x, _y);
}
int Data1::Add(void)
{
return _x + _y;
}
void Data1::ForPrint(void)
{
for (int i = 0; i < 4; i++)
{
_arr[i] = i * i;
printf("%d ", _arr[i]);
}
printf("\n");
}
在函数传参的时候,尤其需要注意拷贝传值和引用传值中拷贝构造函数的动作。
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0)
{
_a = x;
cout << "A(int x = 0)" << _a << endl;
}
A(const A& aa)
{
_a = aa._a;//拷贝
cout << "A(const A& aa)->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
void fun0(A aa)
{
;
}
void fun1(A& aa)
{
;
}
A fun2(void)
{
static A aa;
return aa;
}
A& fun3(void)
{
static A aa;
return aa;
}
int main()
{
A aa0;//使用一次构造函数
A aa1(aa0);//使用一次拷贝构造
fun0(aa0);//由于使用的是拷贝传值方式,所以使用一次自定义的拷贝构造函数,使用完形参后,也要使用自定义析构函数
fun1(aa0);//由于使用的是引用传值方式,无需使用自定义的拷贝构造函数,也就无需使用自定义析构函数
fun2();//内部构建了一个static对象aa,使用了一次构造函数。并且其返回值需要拷贝给类型为A的匿名对象返回来,所以又使用了一次自定义拷贝构造函数,并且这个匿名对象还要使用一次析构函数
fun3();//内部再次构建一个static对象aa,使用了一次构造函数。由于使用的是引用返回,所以无需使用拷贝构造函数
return 0;
//最后aa0使用析构函数,aa1也使用析构函数
//最后把两个static对象aa分别使用析构函数
}
4.3.赋值重载函数
4.3.1.运算符重载
运算符重载的应用很多,比如:对象和对象直接要进行运算符运算。为什么需要重载呢?因为编译器只会内置类型的运算符运算,无法通过运算符运算自定义的类对象,这个时候就需要运算符重载了,例如下面的代码。
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0)
{
_a = x;
cout << "A(int x = 0)" << _a << endl;
}
A(const A& aa)
{
_a = aa._a;//拷贝
cout << "A(const A& aa)->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
void fun0(A aa)
{
;
}
void fun1(A& aa)
{
;
}
A fun2(void)
{
static A aa;
return aa;
}
A& fun3(void)
{
static A aa;
return aa;
}
int main()
{
A aa0;//使用一次构造函数
A aa1(aa0);//使用一次拷贝构造
fun0(aa0);//由于使用的是拷贝传值方式,所以使用一次自定义的拷贝构造函数,使用完形参后,也要使用自定义析构函数
fun1(aa0);//由于使用的是引用传值方式,无需使用自定义的拷贝构造函数,也就无需使用自定义析构函数
fun2();//内部构建了一个static对象aa,使用了一次构造函数。并且其返回值需要拷贝给类型为A的匿名对象返回来,所以又使用了一次自定义拷贝构造函数,并且这个匿名对象还要使用一次析构函数
fun3();//内部再次构建一个static对象aa,使用了一次构造函数。由于使用的是引用返回,所以无需使用拷贝构造函数
return 0;
//最后aa0使用析构函数,aa1也使用析构函数
//最后把两个static对象aa分别使用析构函数
}
面对这种情况,我们就可以使用关键字operator,这个关键字的使用格式是:
返回值类型 operator操作符(参数列表)
{
//…
}
值得注意的是,不能通过连接其他符号来创建新的操作符,比如“operator@”。下面我们来演示一个运算符重载的例子。
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Time
{
public:
//1.构造函数
Time(int x = 2000, int y = 1, int z = 1)
{
_year = x;
_month = y;
_day = z;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
public:
int _year;
int _month;
int _day;
};
//2.全局运算符重载
int operator-(Time x, Time y)
{
return x._year - y._year;
}
int main()
{
Time A;
A.Print();
Time B(2023, 1, 1);
B.Print();
Time C = (B - A);
C.Print();
return 0;
}
另外运算符重载中的重载操作符必须有一个类类型参数(例如两个操作符都是int,这是不被允许的)。
还有五个运算符不允许重载的:
.
∗
、
:
:
、
s
i
z
e
o
f
、
?
:
、
.
.*、::、sizeof、?:、.
.∗、::、sizeof、?:、.
4.3.2.运算符重载函数
而运算符重载有两种写法,一种是全局运算符重载,这种就需要把操作符需要的所有操作数写成参数列表写清楚。
但是如果想上面这么写的话,万一类里的成员变量并不是公有的而是私有的怎么办呢?全局运算符重载是没有办法直接访问类的私有成员的,因此C++还提供了第二种写法:直接写在类里成为类的成员函数,即“运算符重载函数”。
运算符重载函数是具有特殊函数名的函数,它是类里一个特殊的成员函数。
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Time
{
public:
//1.构造函数
Time(int x = 2000, int y = 1, int z = 1)
{
_year = x;
_month = y;
_day = z;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//2.运算符重载函数
int operator-(Time x)
{
return _year - x._year;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Time A;
A.Print();
Time B(2023, 1, 1);
B.Print();
Time C = (B - A);
C.Print();
return 0;
}
为什么只传递一个参数也能通过呢?不要忘记this指针的存在,上述的代码还可以显式写成下面这个样子(即编译器处理后的样子,自己直接这么写是会报错的)。
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Time
{
public:
//1.构造函数
Time(int x = 2000, int y = 1, int z = 1)
{
_year = x;
_month = y;
_day = z;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//2.运算符重载函数
int operator-(Time* const this, Time x)
{
return this->_year - x._year;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Time A;
A.Print();
Time B(2023, 1, 1);
B.Print();
Time C = (&B - A);
C.Print();
return 0;
}
可以利用运算符重载函数来写一个日期加法,“日期(data)+天数(int)=日期(data)”,感兴趣可以自己实现一下。
4.3.3.赋值运算符重载
这里我们提前展示后面日期类里的赋值运算符重载的写法
#include <iostream>
using namespace std;
class Data
{
public:
//1.构造函数(内联的构造函数)
Data(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//2.拷贝构造函数(使用默认的即可)
//3.赋值运算符重载函数
//3.1.写法1(可以是可以,但是不支持连续赋值)
//void operator=(const Data& d)
//{
// //this指向的是“=”前面的操作数,d是“=”后面的操作是
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
//3.2.写法2(允许连续赋值)
Data& operator=(const Data& d)
{
//this指向的是“=”前面的操作数,d是“=”后面的操作是
if(this != &d)//用地址值判断比较好
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print(void)
{
cout << _year << ":" << _month << ":" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2022, 7, 24);
d1.Print();
Data d2(d1);
d2.Print();
Data d3(2022, 8, 24);
Data d4(2023, 6, 21);
d4 = d1 = d3;//这里就需要用到赋值运算符重载函数,而不是拷贝构造函数,并且注意这里是连续赋值
d3.Print();
d1.Print();
d4.Print();
d2 = d2;//这里使用的是自己给自己赋值,赋值运算符重载可以考虑在这里优化
d2.Print();
}
需要注意的是,赋值运算符函数不同于其他的运算符重载函数,赋值运算符重载只能重载为类的成员函数,不能重载成全局函数。如果您这么做了,将会直接报错(error C2801: "operator ="必须是非静态成员)
这样错误的原因本质是:如果您不写赋值运算符重载函数,编译器会生成一个默认的赋值运算符重载函数,内置类型是直接按字节赋值的(因此上面的代码例子实际上是不需要自己写一个赋值运算符重载函数的,编译器自动会把三个int类型的成员进行按字节赋值),而自定义类型成员变量需要调用对应类的赋值运算符重载函数完成赋值(这里很像拷贝构造函数的使用)。
这个时候问题来了,如果写了全局的赋值运算符重载函数,那么将和这个默认生成的赋值运算符重载函数冲突
4.4.取地址重载函数
4.4.1.const成员函数
#include <iostream>
using namespace std;
class A
{
A(a = 1)
{
_a = a;
}
void Print()//这里的this展开写是A* const this
{
cout << _a << endl;
}
int _a;
};
int main()
{
A aa1(5);
const A aa2(10);
aa1.Print();//这里传递&aa1(类型是A*),权限放小,没有关系
aa2.Print();//这里就会有问题
//因为this指针的存在,使得这里调用Print的时候是&aa2(类型为const A*),权限放大了
return 0;
}
因此就需要让编译器给this前面加上const,修改为下面这个代码就可以通过编译了。
#include <iostream>
using namespace std;
class A
{
A(a = 1)
{
_a = a;
}
void Print() const//这里的this展开写就是const A* const this
{
cout << _a << endl;
}
int _a;
};
int main()
{
A aa1(5);
const A aa2(10);
aa1.Print();
aa2.Print();
return 0;
}
4.4.2.取地址及const取地址操作符重载
取地址在默认情况下是很够用的,所以一般是不需要将“&”重载的,除非有的时候不希望被别人取到地址(但是这种应用极少),因此这一部分就暂且忽略了,以后有机会再来补充。
5.日期类的实现
这一部分有些涉及到友元,可以先看我的下一篇C++博文:C++类与对象(下)再回来看看这里日期类的实现。
//Data.h内部
#pragma once
#include <iostream>
#include <cassert>
using namespace std;
class Data
{
//友元
friend ostream& operator<<(ostream& out, const Data& d);
friend istream& operator>>(istream& in, Data& d);
public:
//1.构造函数(内联的构造函数)
Data(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!CheckData())//注意这里爷隐藏了this,经过编译器补充后就是!this->CheckData()
{
Print();
cout << "日期非法" << endl;
}
}
//2.拷贝构造函数(使用默认的即可)
//3.赋值运算符重载函数
//3.1.写法1(可以是可以,但是不支持连续赋值)
//void operator=(const Data& d)
//{
// //this指向的是“=”前面的操作数,d是“=”后面的操作是
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
//3.2.写法2(允许连续赋值)
Data& operator=(const Data& d)
{
//this指向的是“=”前面的操作数,d是“=”后面的操作是
if (this != &d)//用地址值判断比较好
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//4.比较运算符重载函数,只需要写“>、==”和“<、==”的即可,可以不断复用到其他比较运算符重载函数
bool operator==(const Data& d);
bool operator!=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool operator<(const Data& d);
bool operator<=(const Data& d);
//5.加法与加法赋值运算符重载函数
Data& operator+=(int day);
Data operator+(int day);
//Data& operator++(int day);//直接这么重载“++”是没有办法区分前置和后置的,因此C++提供了重载区分
Data& operator++();//前置++
Data operator++(int);//后置++,重载增加一个int参数和前面的前置++构成函数重载进行区分
//6.减法与减法赋值运算符重载函数
Data& operator-=(int day);
Data operator-(int day);
//Data& operator--(int day);//直接这么重载“--”是没有办法区分前置和后置的,因此C++提供了重载区分
Data& operator--();//前置--
Data operator--(int);//后置--,重载增加一个int参数和前面的前置--构成函数重载进行区分
int operator-(const Data& d);
public:
//1.打印函数
void Print(void)
{
cout << _year << ":" << _month << ":" << _day << endl;
}
//2.获取某年某月的天数函数
int GetMonthDay(int year, int month)
{
static int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//静态数组会提高效率,不必重复开辟空间来使用
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
//3.检查日期是否合法函数
bool CheckData()
{
if (_year >= 1
&& (_month > 0 && _month < 13)
&& (_day > 0 && _day <= GetMonthDay(_year, _month)))
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
//7.流运算符重载(不要写成成员函数,不然无法解决this指针的问题)
ostream& operator<<(ostream& out, const Data& d);
istream& operator>>(istream& in, const Data& d);
//Data.cpp内部
#define _CRT_SECURE_NO_WARNINGS 1、
#include "Data.h"
//1.比较运算符重载函数
bool Data::operator==(const Data& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Data::operator!=(const Data& d)
{
return !(*this == d);//这里复用了前面的“==”重载函数
}
bool Data::operator>(const Data& d)
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))//一次性判断是否符合三个条件里的一个
{
return true;
}
else
{
return false;
}
}
bool Data::operator>=(const Data& d)
{
return (*this == d) || (*this > d);
}
bool Data::operator<(const Data& d)
{
return !(*this >= d);
}
bool Data::operator<=(const Data& d)
{
return !(*this > d);
}
//2.加法与加法赋值运算符重载
Data& Data::operator+=(int day)//需要支持连续的“+=”所以有返回值
{
if (day < 0)//这里的if语句是为了预防加上负数的情况
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Data Data::operator+(int day)
{
Data ret(*this);//也可以写Data ret = *this;
ret += day;//复用了前面的“+=”重载函数
return ret;//注意不能使用引用返回,这里是局部变量
}
Data& Data::operator++()
{
return *this += 1;
}
Data Data::operator++(int)
{
Data tmp(*this);
*this += 1;
return tmp;
}
//3.减法与减法赋值运算符重载函数
Data& Data::operator-=(int day)
{
if (day < 0)//这里if语句也是避免减去负数的情况
{
return *this += -day;
}
_day -= day;
while (_day <= 0)//一个月没有0号这个说法
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Data Data::operator-(int day)
{
Data ret(*this);
ret -= day;
return ret;
}
Data& Data::operator--()
{
return *this-=1;
}
Data Data::operator--(int)
{
Data tmp(*this);
*this -= 1;
return tmp;
}
int Data::operator-(const Data& d)
{
int flag = 1;
Data max(*this);
Data min(d);
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
n++;
}
return n * flag;
}
//4.流运算符重载函数(不能写成成员函数,并且注意友元的问题)
ostream& operator<<(ostream& out, const Data& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}//这个函数也推荐写成内联函数,不过就不能声明和定义分离了
istream& operator>>(istream& in, Data& d)
{
in >> d._year >> d._month >> d._day;
assert(d.CheckData());//检查输入的合法性
return in;
}
//main.cpp内部
#define _CRT_SECURE_NO_WARNINGS 1
#include "Data.h"
void TestData1(void)
{
Data d1(2022, 7, 24);
d1.Print();
Data d2(d1);
d2.Print();
Data d3(2022, 8, 24);
Data d4(2023, 6, 21);
d4 = d1 = d3;//这里就需要用到赋值运算符重载函数,而不是拷贝构造函数,并且注意这里是连续赋值
d3.Print();
d1.Print();
d4.Print();
d2 = d2;//这里使用的是自己给自己赋值,赋值运算符重载可以考虑在这里优化
d2.Print();
}
void TestData2(void)
{
Data d5(2000, 1, 1);
Data d6(d5);
Data d7(1900, 1, 2);
Data d8(3000, 5, 4);
if (d5 == d6)
{
if (d5 != d7)
{
if (d5 > d7)
{
if (d5 < d8)
{
cout << "成功!" << endl;
}
}
}
}
}
void TestData3(void)
{
Data d9(2001, 10, 28);
d9.Print();
d9 += 20;
d9.Print();
(d9 + 3).Print();
Data d10 = d9 + 4;
d10.Print();
(++d10).Print();//Data operator++(&d10);//前置++
(d10++).Print();//Data operator++(&d10, 0);//后置++,这里传0,但其实传什么值不重要,只要编译器能做区分就行
d10.Print();
}
void TestData4(void)
{
Data d11(2022, 7, 25);
Data d12(2000, 2, 15);
cout << d12 - d11 << endl;
(d11-3).Print();
(d11--).Print();
d11.Print();
(--d11).Print();
}
void TestData5(void)
{
Data d12(2023, 12, 40);
}
void TestData6(void)
{
Data d13(2001, 2, 5);
Data d14(2009, 3, 4);
Data d15;
Data d16;
//cout << d13 << endl;
//这里不能直接使用cout
//是因为<<也是运算符
//只对内置类型重载(库里面写好的)
//这里没有办法识别d13为何种类型
//无法匹配得到输出
//写了重载后
cout << d13 << d14;//cout也是一个对象
cin >> d15 >> d16;
cout << d15 << d16;
}
int main()
{
cout << "测试例1:" << endl;
TestData1();
cout << endl << "测试例2:" << endl;
TestData2();
cout << endl << "测试例3:" << endl;
TestData3();
cout << endl << "测试例4" << endl;
TestData4();
cout << endl << "测试例5" << endl;
TestData5();
cout << endl << "测试例6" << endl;
TestData6();
return 0;
}
这里留一个小的补充:如何计算某一天是周几呢?(注意历史上有个时间点可能删除了几天)
6.补充:命名方法规范
这里补充一下常见的变量名、对象名、类名
、函数名的命名方法:大小驼峰法。
- 函数名、类名大驼峰
- 普通变量小驼峰
- 成员变量首单词从“_”开始