类的基础概述1
约 12505 字大约 42 分钟
2025-06-21
1.面向过程和对象
1.1.面向过程
C语言就是一种面向过程的语言,如果您经常使用C解决问题,就会发现在编程的过程中我们非常注重细节和步骤,通过各种块级功能的函数调用逐步解决问题。
1.2.面向对象
C++语言,是面向对象和面向过程的一种混编语言,在解决问题时,关注的是对象,将一件事情拆分为不同的对象,靠对象之间的交互完成。
例子:我想煮饭,于是需要对象:我、电饭煲、菜,“我”把“菜”放入“电饭锅”,不需要了解我是通过哪一块肌肉放入菜,不需要了解菜是具有什么分子结构,不需要了解电饭煲的工作原理,煮饭都是靠“我”、“菜”、“电饭煲”三者(三个对象)交互完成的。
2.类的引入
2.1.类和结构
C语言的结构体只能定义变量,而结构体不仅可以定义变量,也可以定义函数。
但是对于结构体在内部加入函数的方式,C++更喜欢用class来代替struct。
使用struct是为了和C兼容使用。
2.2.类的使用
下面我们在C++代码中使用struct来定义一个基础的C++类/结构体(在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(也就是struct的升级版,功能更加强大,至于强大在哪里我们后续会提及)。
//类和对象的使用
#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的struct)。
在C中,stack结构体可以创建出一个结构体变量,C++的class类也同样可以,但是需要注意的是,由类创建出来的变量不是被称为“类变量”,而被称做“对象”。
通过对象可以像以前的结构体变量一样访问成员变量,甚至是调用成员函数(例如:上面C++代码中的X.Xadd(1, 2)就是通过X这个对象来调用成员函数Xadd()的)。
补充:命名方法规范
在
C++中,有一些常见的命名方法,我们整篇系列的命名以大小驼峰法来命名。
函数名、类名大驼峰
普通变量小驼峰
成员变量首单词从
_开始
2.3.类限定符
在了解类限定符之前,我们先来了解一下面向对象语言三大特性之一的“封装”。
2.3.1.类封装的概念
面向对象的三大特性:封装、继承、多态(这三个是最重要的,但是面向对象还有其他的特性,例如“抽象”)。封装的本质是以一种管理,封装将数据和操作数据的方法结合起来,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装让用户更加方便使用类。而C++通过访问限定符来控制隐藏对象的细节,控制哪些方法可以在类外部直接使用。
2.3.2.限定符的使用
public(公有),public修饰的成员在类外可以直接被访问protected(保护),protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)private(私有),protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
补充:
public成员在类内外均可访问,protected成员只能在类内和派生类中访问,而private成员仅能在类内访问。 三个关键字的范围是从使用访问限定符开始,直到遇到另外一个访问限定符这类的}结尾时结束。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;
}注意:访问限定符只有在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
在没有见到继承等概念之前,我们的C++程序会出现大量的public和private这两个限定符,您只需要重点了解这两个就行。
2.4.类作用域
类的出现赋予了C++一个新得作用域:类的作用域,即“类域”。
在类的里面,类的所有成员都在类的作用域中
在类外面定义成员时,需要使用
::作用域操作符来指明
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}需要注意的是,哪怕有访问限定符,只要使用的是同属一个类内的函数或遍历,就不受限定符的影响。
#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.5.类的定义
2.5.1.类中定义函数
声明放在类中,定义也放在类中。
class 类名
{
public:
//某些方法(包括声明和定义)
public:
//某些属性
}需要注意的是,如果成员函数在类中直接定义,编译器可能当成内联函数处理。因此可以利用这一点,较小的函数若要做内联,则直接写到类内部。
2.5.2.类外定义函数
声明放在类中,定义放在类外。
//类头文件class.h中
class 类名
{
public:
//某些方法基本信息(函数声明)
public:
//某些属性
}//类源文件class.c中
#include "class.h"
函数返回值 类名::函数名字()
{
//某些方法定义(函数方法)
}一般来说后者会比较规范一些(就是需要注意一些语法细节),但为了更加好演示代码,后续的文章大部分采用第一种,正式工作时能用第二种就用第二种。
而且可以尝试区分好方法名字和属性名字,比如在属性(类里的成员变量)前加上下划线_、函数名/类名采用大驼峰命名、变量名采用小驼峰命名。
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实际就是该类中“成员变量”之和,不过要注意内存对齐的问题!这点和结构体内存对齐是一样的...因此我们可以观察到:对象在计算大小中是不算上成员函数的。
为什么不带上成员函数的大小呢?不带上的话,函数需要的空间又应该保存在哪里呢?让我们假设下面三种情况:
对象存储方法一:存储“类函数+类成员变量”假设大小含有成员函数,那么每次都实例化一个类生成一个对象时,就会重复多出函数的栈帧空间,浪费空间
对象存储方法二:存储“类函数表地址+类成员变量”,其中类函数表地址指向一个类函数表(这在虚表和多态被采用),这种方案不错,但是
C++没有采用这种对象存储方法三:只保存成员变量,成员函数存放在公共的代码段。编译链接的时候就会根据函数名去公共代码区找到函数的地址(类似
a.Function()时,汇编的时候会使用call指令去替换,即:call函数地址),这是C++所采用的
注意:类成员函数表(
Class Member Function Table)和公共代码区域(Common Code Area)是两个不同的概念。
类成员函数表:类成员函数表是用于存储与类相关的成员函数指针的数据结构。每当创建一个类的对象时,都会为该类分配一个内存空间,并在内存中创建一个类成员函数表。类成员函数表中的每个条目都保存着对应成员函数的地址。通过类成员函数表,可以在运行时动态地调用类的成员函数。这种机制称为虚函数表(
VTable)或虚函数指针表(VPointer Table),它支持面向对象编程中的多态性。公共代码区域:公共代码区域是指程序中多个模块(例如函数、方法或类)之间共享的代码部分。当多个模块需要访问相同的功能或过程时,它们可以共享一段代码,而不是复制相同的代码到每个模块中。这样做可以减少冗余,并提高代码的重用性和可维护性。公共代码区域通常位于内存的某个特定位置,所有需要访问这些公共代码的模块都可以通过指针或跳转进行调用。
总结起来,类成员函数表是一个与类相关的数据结构,用于支持多态调用类的成员函数。而公共代码区域则是指多个模块之间共享的代码部分,可以提高代码的重用性和可维护性。两者是面向对象编程和模块化设计中不同概念的体现。
使用下面代码也能证明:函数没有在类中存储。
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;
}另外这里需要注意空类(没有任何成员函数和成员变量的类)的大小,空类比较特殊,C++规定空类使用一个字节大小的空间来唯一标识这个类的对象,只做占位使用,内部不存储实际数据。 为什么说是做标记使用呢?因为如果开辟0字节,那就没有指针可以指向这个空间了,下面两个对象就无法做出区分:
class Data
{};//空类
Data d1;//空类创建出来的成员1
Data d2;//空类创建出来的成员2
printf("%zd", sizeof(Data));//输出1
printf("%p", &d1);//d1和d2存储在两个地方
printf("%p", &d2);3.this指针
我们之前说过,类是创建对象的一个“模板”,但是在类中存在成员函数,并且我们通过对象来调用成员函数的时候,有一个很奇怪的现象。
3.1.隐藏this指针
class Data
{
public:
void Init(int year, int month, int day)//3.但是函数是怎么找到实例化后,变量里的_year呢?其实就是在这里的参数列表隐藏了一个this指针(每一个成员函数都有)完整写法为:void Init(Date* const this, int year, int month, int day)
{
_year = year;//2.而这里的year值并不是赋给了类里的_year,而是实例化后对象d里的_year,这句话的完整写法为:this->_year = year;
_month = month;//这句话的完整写法为:this->_month= month;
_day = day;//这句话的完整写法为:this->_day= day;
}
void Print(void);
private:
int _year;//1.没有经过实例化的类,因此这里的_year只是声明
int _month;
int _day;
};
void Data::Print(void)//这里没有添加this指针
{
cout << _year << "-" << _month << "-" << _day << endl;//这个函数到底打印的是哪一个对象的成员变量???
}
int main()
{
Data d;
d.Init(2023, 6, 8);
d.Print();
return 0;
}this指针参数由编译器帮我们加上,在对象d调用函数Init()的时候,函数参数内部隐含一个this指针参数,指向对象d,确保初始化的对象是d的成员变量。
但是在类外定义的函数不会自动添加this指针,因此调用Print()函数的时候,函数的参数列表内部没有隐藏的this指针,因此不知道指向哪一个对象,这样在打印成员变量的时候,该函数就会失败。
下面为了方便理解,我们把编译器的活也给补上(注意下面的代码是运行不起来的,只是我们演示了在编译器视角是怎么工作的)
/*
class Data
{
public:
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print();
private:
int _year;
int _month;
int _day;
};
void Data::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int main()
{
Data d;
d.Init(&d1, 2023, 6, 8);
d.Print(&d1);
return 0;
}
*/3.2.显示this指针
this指针我们不能手动加到函数参数列表,或者函数调用中(即:不能显示接受和显示传递this指针),这是编译器在做的事情,编译器会帮您自动加上。
但是有一个例外:您可以在函数内部使用this指针,这点C++没做太多限制,唯一需要的是由于this指针参数的类型是类类型 const*,因此没有办法使用解引用改变this指向对象的值。
这种情况经常出现在类外部定义的函数,由于声明在类内部,因此已经添加了this指针,定义处的参数列表也不需要再声明有一个this指针的存在,但是函数定义内的函数体没有自动添加this指针,需要我们手动加上。
class Data
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print();
private:
int _year;
int _month;
int _day;
};
void Data::Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;//手动调用,这里的this是我们自己写出来的,不是编译器加上来的
}
int main()
{
Data d;
d.Init(2023, 6, 8);
d.Print();
return 0;
}3.3.this指针的特性
class Cl
{
public:
int x;
int y;
public:
void Print()//2.程序来到这里后,这里的函数头等价于“Print(Cl* const this)”
{
cout << x << endl;//3.上面这一句展开来写就是“cout << this->x << endl;”这个时候就发生空指针解引用了,程序就会发生奔溃
}
};
int main()
{
Cl* C = nullptr;
C->Print();//1.这里展开写就是“C->Print(C);”,这个函数被call的地址在编译的时候就被写入了,故正常调用(函数的地址并没有被存在对象里面,这里的解引用是“假象”)
return 0;
}另外,这个this指针补全了就是类类型* const this,这里的const只是避免了改变this的值,但是没有避免改变*this(也就是对象本身)的值,但是有的时候我们不希望this在编写代码的过程中不小心改变this指向对象的值,但是如果我们希望再加一个const(也就是成为类类型* const this)来避免this改变指向怎么办呢?这个this指针被编译器隐藏起来了呀!
因此我们可以直接在调用了this指针的函数签名的最后面加上const关键字,因此如果是上面的void Print(),在声明处可以改写成void Print() const,而在函数定义处也需要这么更改。
这种现象在本文的末尾我们还会再次提到。
#include <iostream>
#include <string>
using namespace std;
class Data
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print() const;
private:
int _year;
int _month;
int _day;
};
void Data::Print() const
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
int main()
{
Data d;
d.Init(2023, 6, 8);
d.Print();
return 0;
}3.4.this指针的位置
那么this指针应该存储到哪里去呢?存储到栈的区域,因为this本质还是一个形参,存储在栈上。但是这是不一定的,有的编译器会进行优化,比如:VS2022一般会采用寄存器ecx去优化代码,这点可以查看反汇编看看即可。
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 ret4.类的默认成员函数
实际上,一个空类并不是真的“空”,编译器还会自动生成6个默认成员函数(默认成员函数是用户没有显式实现的。编译器会自动生成的成员函数称为默认/缺省成员函数)
class ClassName
{
//某些成员
};初始化和清理:构造函数完成初始化、析构函数完成清理
拷贝复制:拷贝构造函数使用同类对象初始化创建对象、赋值重载函数主要是把一个对象赋值给另外一个对象
取地址重载:主要是重载“普通对象”和“
const对象”取地址,这两个很少会自己实现,目前还不太重要(有机会再细谈)
4.1.初始化和清理
4.1.1.构造函数
构造函数是类里特殊的成员函数,虽然叫“构造”,但是它的工作不是开辟空间,而是在构造对象的时候自动被调用,进行对象的初始赋值工作(构造函数这个名字其实起得不够好,容易引发误会)。
构造函数可以帮助我们给与对象的成员变量初始赋值,而不至于因为忘记初始赋值变量而得到随机值或者崩溃的结果。
由于这个函数很特殊,所以在理解的时候可以特殊化理解,在调用的时候也要特殊使用。
注意:这里说的是初始值赋值,而不是初始化!
原因是初始化只有一次,但是赋值可以多次,构造函数的作用类似于帮助我们在定义
int a之后,编译器进行了a=初始值的功能。而不是实现了在定义a的同时初始化a,即int a = 初始值。这两种方式是完全不一样的,而这一点我们将在初始化列表的时候再次提及,这里学习构造函数的时候您只需要知道如何使用即可。
构造函数的特征如下:
- 函数名和类相同
- 没有返回值,也不用写
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++早期设计的一个缺陷,当然有的新编译器对这些进行了处理,但是我们不能太依赖编译器的优化。
但是在C++ 11的时候打了一个补丁,可以通过给类内置类型成员变量设置缺省值来实现初始化,注意:是初始化,不是初始赋值。
//默认构造函数对内置类型不起作用
class Cl
{
public:
Cl(int x = 1)//C1类创建对象时使用的默认构造函数
{
_x = x;
}
public:
int _x;
};
class Date
{
public:
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、_a2自动进行了初始化,但是其他的三个内置类型没有初始化
return 0;
}//默认构造函数对内置类型不起作用
class Cl
{
public:
Cl(int x = 1)
{
_x = x;
}
public:
int _x;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << _a1._x << " " << _a2._x << endl;
}
private:
Cl _a1;
Cl _a2;
int _year = 2000;//补丁
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();//观察到都自动进行了初始化
return 0;
}注意:这个补丁的缺省参数还有一个离谱的用法:可以在缺省处调用函数,例如使用
malloc()。
总结起来,实际上在C++中默认构造函数有三类:
我们没写,编译器自动生成的默认构造函数
我们自己写的,全缺省的默认构造函数
我们自己写的,无参的默认构造函数
另外,这三种默认构造函数不可能同时存在。
在C++中,构造函数是可以使用private访问权限进行修饰的。这种情况下,它们通常被用于实现特定的设计模式,如:“单例模式”或“工厂模式”等。
使用private构造函数可以防止类的实例化,也就是禁止用户直接创建对象。这样做的目的可能是为了控制对象的创建和确保只有特定的方式(即特点的参数)能够创建对象,避免用户错误创建具有危险性的对象。
补充:以下是一个使用
private构造函数实现单例模式的示例:class Singleton { private: Singleton() {}//私有构造函数 public: static Singleton& getInstance() { static Singleton instance; return instance; } }; int main() { //Singleton obj;//错误!无法直接创建对象 Singleton& obj = Singleton::getInstance();//通过静态函数获取对象实例 return 0; }在上述示例中,私有构造函数
Singleton()阻止了类实例的直接创建,而是通过静态成员函数getInstance()来获取唯一的类实例。
同时还要强调,
C++不允许类类型 对象名字()这种写法,因为编译器没有办法确认这是定义一个对象,还是对一个函数进行声明。这样做是不会调用默认构造函数的,只需要把括号去掉就可以。
补充:
C++11中新增了委托构造的用法,允许一个构造函数在调用之前,先使用同类的其他重载过的构造函数。// 委托构造的使用 #include <iostream> class MyClass { public: // 委托构造函数 MyClass() : MyClass(0) { std::cout << "Value: " << _x << ", " << _y << std::endl; } MyClass(int x) : MyClass(x, 0.0) { std::cout << "Value: " << _x << ", " << _y << std::endl; } MyClass(int x, double y) : _x(x), _y(y) { std::cout << "Value: " << _x << ", " << _y << std::endl; } private: int _x; double _y; }; int main() { MyClass obj1; // 调用无参构造函数,委托给 MyClass(0, 0.0) MyClass obj2(5); // 调用带一个参数的构造函数,委托给 MyClass(5, 0.0) MyClass obj3(10, 3.14); // 直接调用带两个参数的构造函数 return 0; }
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>
#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;
};
void test(void)
{
Data1 C;
C.Print();
printf("%d\n", C.Add());
C.ForPrint();
}
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");
}但是,如果我们没有显式的写出析构函数,那么编译器自己执行的默认析构函数又有什么作用呢?和构造函数类似,隐式的析构函数:对内置类型不做处理,但是会处理自定义类型,调用字段自定义类型的默认析构函数,这里的默认析构函数包括编译器自己生成的析构函数、我们自己写的自定义析构函数。
其中编译器默认生成的析构函数的作用就是调用自定义类型的默认构造函数。
#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的垃圾回收机制是不一样的,不能混为一谈!利用析构函数在书写某些数据结构的时候就很方便。
下面演示构造函数和析构函数的调用顺序问题:
#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等我们学到在
C++中栈和队列的使用,就可以使用C语言和C++语言来解决这道力扣题目:力扣:两个栈实现一个队列对比一下,对比一下C++的便捷之处。
4.2.拷贝复制赋值
4.2.1.拷贝构造函数(初始化的拷贝)
在创建对象时,可否创建一个与已存在对象相同的的新对象呢?这个时候就可以用到拷贝构造函数,拷贝构造函数的特征有:
只有单个形参,该形参是对本类型对象的“引用”(一般常用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.2.2.赋值运算符重载函数(赋值拷贝)
要使用赋值重载函数,还需要先深入了解运算符重载函数(Java没有运算符重载)。
运算符重载的应用很多,比如:对象和对象直接要进行运算符运算。为什么需要重载呢?因为编译器只会内置类型的运算符运算,无法通过运算符运算自定义的类对象,这个时候就需要运算符重载了,例如下面的代码。
//没有运算符重载
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Time
{
public:
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;
};
int main()
{
Time A;
A.Print();
Time B(2023, 1, 1);
Time C = B - A;//这里的编译器就无法知道究竟如何运算
C.Print();
return 0;
}面对这种情况,我们就可以使用关键字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,这是不被允许的)。
还有五个运算符不允许重载的:.*、::、sizeof、?:、.
运算符重载有两种写法,一种是全局运算符重载(在类外声明和定义),这种就需要把操作符需要的所有操作数写成参数列表写清楚(因为没有this指针)。
但是如果像上面这么写的话,万一类里的成员变量并不是公有的而是私有的怎么办呢?全局运算符重载是没有办法直接访问类的私有成员的(除非使用友元),因此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),感兴趣的话您可以自己先实现一下,或者直接看下一章节的《日期类的实现》。
接下来我们来看看赋值运算符重载的使用。 这里我们提前展示后面日期类里的赋值运算符重载的写法(之后会详细介绍日期类的实现)
#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.3.取地址重载函数
4.3.1.const成员函数
C++的类型检查很严格,对于const对象,不可以直接使用没有const修饰this指针的成员函数。但是this是编译器自动给我们加上的,我们在哪里写const呢?答案是直接写在成员函数的函数头右侧(这种现象我们之前提到过)。
//没有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,修改为下面这个代码就可以通过编译了。
//写了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;
}注意:
const加到普通的函数返回值是没有什么意义的,因为正常情况下,函数的返回值具有常属性,没有必要再多此一举加上const。
思考一下下面的几个问题:
const对象可以调用非const成员函数吗?(不可以)非
const对象可以调用const成员函数吗?(可以,发生隐式转化)const成员函数内可以调用其他非const成员函数吗?(不可以,本质是const的this调用了非const的成员函数,这符合上面的第1条。但是可以调用const成员变量,不过这会造成一个现象:声明了一个const成员函数,就很有可能需要声明一系列的const函数)非
const成员函数可内以调用其他const成员函数吗?(可以,本质是非const的this调用了const的成员函数,这符合上面的第2条)
给某些只读的成员函数加上const修饰this最重要的好处是:const对象可以正常调用成员函数,不用担心在编码的时候被人为破坏。
注意:声明和定义在加
const上要同步加上。
4.3.2.取地址操作符重载
取地址在默认情况下是很够用的,所以一般是不需要将&重载的,除非有的时候不希望被别人取到有地址(但是这种应用极少,不过也不是没有)。
对于自定义类型,理论上是不能直接使用&的,但是我们在编译器中是能通过编译并且运行的。这是因为类还有一个默认成员函数:取地址重载成员函数。我们可以显示写出下面得成员函数:
class Data
{
public:
Data* operator&()
{
return this;
}
const Data* operator&()const
{
return nullptr;
}
};可以返回一个虚假的地址...
5.日期类的实现
这里我实现了一个简单的日期类,您可以去看看。