类的基本思想是数据抽象( d a t a a b s t r a c t i o n data\ abstraction data abstraction)和封装( e n c a p s u l a t i o n encapsulation encapsulation)。数据抽象是一种依赖于接口( i n t e r f a c e interface interface)和实现( i m p l e m e n t a t i n o implementatino implementatino)分离的编程技术。对象是类的实例,类是对象的模板。
文章目录
- 一、类的定义
- 1. 类的概念及定义
- 2. 访问限定符
- 3. 类域
- 二、实例化
- 1. 实例化概念
- 2. 对象大小
- 三、this 指针
- 四、类的默认成员函数
- 1. 构造函数
- 【补充1】初始化列表
- 【补充2】类型转换
- 2. 析构函数
- 3. 拷贝构造函数
- 4. 赋值运算符重载
- 4.1 运算符重载
- 4.2 赋值运算符重载
- 5. 取地址运算符重载
- 5.1 const 成员函数
- 5.2 取地址运算符重载
- 五、static 成员
- 六、友元
- 七、内部类
- 八、匿名对象
- 九、对象拷贝时编译器的优化(了解)
- 完整代码示例
- 1. C++ 和 C 语言实现 Stack 对比(封装)
- 2. 日期类的实现(运算符重载)
- 总结
一、类的定义
类关键字是 c l a s s class class,定义一个类和定义一个结构体类似,但是类里面不仅可以放变量,还可以放函数。
C++ 中 s t r u c t struct struct 也可以定义类,C++ 兼容 C 中 s t r u c t struct struct 的用法,同时 s t r u c t struct struct 升级成了类,明显的变化是 s t r u c t struct struct 中可以定义函数,一般情况下我们还是推荐用 c l a s s class class 定义类。
// C++升级struct升级成了类
// 1、类⾥⾯可以定义函数
// 2、struct名称就可以代表类型
// C++兼容C中struct的⽤法
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}LTNode;// 不再需要typedef,ListNodeCPP就可以代表类型
struct ListNodeCPP
{void Init(int x = 0){next = nullptr;val = x;}ListNodeCPP* next;int val;
};int main()
{LTNode nc;ListNodeCPP ncpp;ncpp.Init();cout << ncpp.val << endl; // 0return 0;
}
1. 类的概念及定义
类中的内容称为类的成员:
- 类的变量称为类的属性或成员变量。
- 类的函数称为类的方法或成员函数。
注意:类定义结束时后⾯分号不能省略(和结构体类似)
例如,用 C++ 的类封装一些 S t a c k Stack Stack 的简单功能:
class Stack
{
public:// 成员函数(方法)void Init(int n = 4){a = (int*)malloc(sizeof(int) * n);capacity = n;top = 0;}void Push(int x){// ...扩容a[top++] = x;}int Top(){return a[top - 1];}void Destroy(){free(a);a = nullptr;top = capacity = 0;}private: // 成员变量(属性)int* a;size_t capacity;size_t top;}; // 分号不能省略int main()
{Stack st; st.Init();st.Push(1);st.Push(2);cout << st.Top() << endl;st.Destroy();return 0;
}
成功调用四个函数,最终结果应该返回栈顶值 2 2 2 :
为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加 _ \_ _ 或者 m m m 开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。
用一个日期类来举例:
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:// 为了区分成员变量,⼀般习惯上成员变量// 会加⼀个特殊标识,如_ 或者 m开头int _year; // year_ m_yearint _month;int _day;
};
int main()
{Date d;d.Init(2024, 3, 31);return 0;
}
这里主要是为了和初始化函数 I n i t ( ) Init() Init() 的形参变量作区分。(这里命名都希望使各变量意义明确,因此才会命名冲突)
- 注意:定义在类里面的成员函数默认为 i n l i n e inline inline(声明和定义分离就不是内联了,定义函数的时候需要加域作用限定符)。
2. 访问限定符
C++ 一种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
- p u b l i c public public
p u b l i c public public 修饰的成员在类外可以直接被访问。
- p r i v a t e private private 和 p r o t e c t e d protected protected
p r o t e c t e d protected protected 和 p r i v a t e private private 修饰的成员在类外不能直接被访问。( p r o t e c t e d protected protected 和 p r i v a t e private private 是一样的,以后继承章节才能体现出他们的区别)
一般成员变量都会被限制为 p r i v a t e / p r o t e c t e d private/protected private/protected,需要给别人使用的成员函数才会放为 p u b l i c public public。
class Stack
{
public:void Init(int n = 4){a = (int*)malloc(sizeof(int) * n);capacity = n;top = 0;}void Push(int x){// ...扩容a[top++] = x;}int Top(){return a[top - 1];}void Destroy(){free(a);a = nullptr;top = capacity = 0;}
private:int* a;size_t capacity;size_t top;
};int main()
{Stack st; //成员函数都是public权限(可以访问)st.Init();st.Push(1);st.Push(2);cout << st.Top() << endl;st.Destroy();//成员变量都是private权限(不可以访问)st.a = nullptr; //error C2248: “Stack::capacity”: 无法访问 private 成员(在“Stack”类中声明)st.capacity = 0; //error C2248: “Stack::capacity”: 无法访问 private 成员(在“Stack”类中声明)st.top = 0; //error C2248: “Stack::top”: 无法访问 private 成员(在“Stack”类中声明)return 0;
}
- 访问权限作用域: 从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。(如果后面没有访问限定符,作用域就到 } \} } 即类结束)
- 定义成员没有被访问限定符修饰时: c l a s s class class 默认为 p r i v a t e private private, s t r u c t struct struct 默认为 p u b l i c public public。
3. 类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 : : :: :: 作用域操作符指明成员属于哪个类域。
class Stack
{
public:void Init(int n = 4);private:int* a;int capacity;int top;
};//编译错误
void Init(int n = 4)
{a = (int*)malloc(sizeof(int) * n); //error C2065: “a”: 未声明的标识符capacity = n; //error C2065: “capacity”: 未声明的标识符top = 0; //error C2065 : “top”: 未声明的标识符
}//正确写法:声明和定义分离,需要指定类域
void Stack::Init(int n)
{a = (int*)malloc(sizeof(int) * n);capacity = n;top = 0;
}int main()
{Stack st;st.Init();return 0;
}
类域影响的是编译的查找规则,如果不指名类域,编译器就会默认当作全局变量或全局函数;只有指定类域,编译器才会当作成员函数或成员变量。
二、实例化
类好比是一个图纸,而实例化产生的对象就是按照图纸建造的房子。
类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
1. 实例化概念
⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
这里还是用日期类来举例:
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和d2Date d1, d2;d1.Init(2025, 2, 28);d1.Print();d2.Init(2025, 3, 31);d2.Print();return 0;
}
打印出来的信息就是对象示例化后的信息:
⼀个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
2. 对象大小
类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?
首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。
第一种方式当然是全部存储,将成员变量和成员函数指针都存储起来,但如果实例化 10000 10000 10000 个对象,则需要存储 10000 10000 10000 个成员函数指针,而这些函数指针都指向同一块空间,函数指针被重复存储了 10000 10000 10000 次!这显然浪费空间。
其实函数指针是不需要存储的,函数指针是⼀个地址,调用函数被编译成汇编指令 [ c a l l 地址 ] [call\ 地址] [call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,才需要存储函数的地址。
因此就有了第二种存储方式 —— 只存放类的成员变量,类的成员函数则放在公共代码区:
上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。
内存对齐规则:
- 第一个成员在结构体(类)偏移量为 0 0 0 的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = = m i n ( 编译器默认对齐数 ( V S 默认对齐数为 8 ) , 该成员变量大小 ) 对齐数 == min(编译器默认对齐数(VS默认对齐数为8),该成员变量大小) 对齐数==min(编译器默认对齐数(VS默认对齐数为8),该成员变量大小)
结构体(类)总大小为:最大对齐数( m i n ( 编译器默认对齐数 , m a x { 所有变量类型 } ) )的整数倍。 结构体(类)总大小为:最大对齐数( min(编译器默认对齐数,max\{所有变量类型\})\ )的整数倍。 结构体(类)总大小为:最大对齐数(min(编译器默认对齐数,max{所有变量类型}) )的整数倍。
如果嵌套了结构体(类)的情况:
- 嵌套的结构体(类)对齐到自己的最大对齐数的整数倍处
- 结构体(类)的整体大小就是所有最大对齐数(含嵌套结构体(类)的对齐数)的整数倍。
// 计算⼀下A/B/C实例化的对象是多大?//A:既有成员函数,又有成员变量
class A
{
public:void Print(){cout << _ch << endl;}
private: //因此最大对齐数为:4 Byte(2个成员变量因此对象大小为:8)char _ch; //大小为 1 Byteint _i; //大小为 4 Byte
};//B:只有成员函数
class B
{
public:void Print(){//...}
};//C:空
class C
{
};int main()
{A a; B b; C c;cout << sizeof(a) << endl; // 8cout << sizeof(b) << endl; // 1cout << sizeof(c) << endl; // 1return 0;
}
运行结果为:
通过上述结果我们发现:没有成员变量的 B B B 和 C C C 类对象的大小是 1 1 1。
为什么没有成员变量还要给 1 个字节呢?
因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。
三、this 指针
既然类中只存储成员变量,成员函数都存放到了公共代码区,那么 我们在创建的不同对象要访问同一个函数的时候应该如何区分呢? 那么这里就要看到C++给了一个隐含的 t h i s this this 指针解决这里的问题。
这里我们还是用日期类来举例:
class Date
{
public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){this->_year = year;this->_month = month;this->_day = day;}// void Print(Date* const this)void Print(){cout << this->_year << "/" << this->_month << "/" << this->_day << endl;}private:// 这里只是声明,没有开空间int _year;int _month;int _day;
};int main()
{Date d1, d2;// d1.Init(&d1, 2025, 3, 9)d1.Init(2025,3,9);// d2.Init(&d1, 2005, 8, 23)d2.Init(2005,8,23);// d1.Print(&d1)d1.Print();// d2.Print(&d2)d2.Print();return 0;
}
编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针 Date* const this
,叫做 t h i s this this 指针。
在各个对象调用函数的时候会默认把当前对象的地址传给第一个参数。(这里用日期类定义的的对象 d 1 d_1 d1 来举例:d1.Init(&d1, 2025, 3, 9)
)
这样在函数调用的时候,每个函数都能通过 t h i s this this 指针来找到对应对象的成员变量。this->_year = year;
注意: C++规定不能在实参和形参的位置显式地写 t h i s this this 指针(编译时编译器会处理),但是可以在函数体内显式使用 t h i s this this 指针。
有一个面试题这么考过:
Q : t h i s 指针存在内存哪个区域的( A ) Q:this指针存在内存哪个区域的 (\ A\ ) Q:this指针存在内存哪个区域的( A )
A . 栈 B . 堆 C . 静态区 D . 常量区 E . 对象里面 A. 栈\ \ B.堆\ \ C.静态区\ \ D.常量区\ \ E.对象里面 A.栈 B.堆 C.静态区 D.常量区 E.对象里面
因为 t h i s this this 指针实际上是形参,因此会存在函数栈帧中,所以选 A A A 是没问题的。(但是由于 t h i s this this 指针会频繁使用, V S VS VS 编译器会将他存到寄存器中进行优化)
注意: 这里不能选 E E E 哈,因为对象里面只存成员变量,连成员函数指针都不会存,更别提形参了。
看下面一段程序:
class A
{
public:void Print() //这里 this == nullptr ,如果解引用就会报错 {cout << "A::Print()" << endl;//对空指针解引用程序崩溃(不解引用就没事)cout << _a << endl; //cout << this->_a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;//p是指向A类对象的指针且指向空p->Print(); //直接传p的值当作this指针return 0;
}
如果类对象的地址为空的话( t h i s = = n u l l p t r this == nullptr this==nullptr),则不能访问成员变量。(会对 t h i s this this 空指针解引用导致程序崩溃)
四、类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
一个类,我们不写的情况下编译器会默认生成以下 6 6 6 个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载、普通对象取地址重载 和 c o n s t const const 对象取地址重载。(需要注意的是这 6 6 6 个中最重要的是前 4 4 4 个,最后两个取地址重载不重要,稍微了解一下即可)
C++11 以后还会增加两个默认成员函数:移动构造和移动赋值。
我们在学习默认成员函数的时候要始终怀揣着两个问题:
- 我们不写时,编译器 默认生成的函数行为是什么? 是否满足我们的需求?
- 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
1. 构造函数
构造函数( C o n s t r u c t o r Constructor Constructor)的主要任务是对象实例化时初始化对象,而不是开空间创建对象。(我们常使用的局部对象是栈帧创建时空间就开好了)
构造函数的本质是要替代我们以前Stack类和Date类中写的Init()函数(初始化函数)的功能,构造函数自动调用的特点就完美的替代的了Init()函数。
这里还是以日期类来举例:
class Date
{
public://1.无参构造函数Date(){_year = 2000;_month = 1;_day = 1;}//2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}//3.全缺省构造函数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;Date d2(2025);Date d3(2025, 3, 10);d1.Print(); // 2000/1/1d2.Print(); // 2025/1/1d3.Print(); // 2025/3/10return 0;
}
构造函数的特点:
-
函数名与类名相同。
-
无返回值。(也不需要写 v o i d void void)
-
对象实例化时,系统会自动调用对应的构造函数。
-
构造函数可以重载。(无参和全缺省构成函数重载,但调用时会产生歧义,因此不能同时存在)
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个(无参的)默认构造函数。(一旦用户显式定义编译器将不再生成)
-
无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。(调用时会存在歧义)
默认构造函数:不传实参就直接能自动调用的,就叫默认构造。(带参和半缺省都必须传参数)
- 对于内置类型成员变量,编译器对其是否初始化是不确定的;而对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化,这样编译器生成的无参默认构造函数就能直接自动调用自定义类型的默认构造函数(因为不需要手动传参)。
typedef int STDataType;class Stack
{
public: //应该写无参或者全缺省或者不写 Stack(int n = 4)Stack(int n) //带参构造函数不是默认构造函数,因此会报错{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}
private:STDataType* _a;int _capacity;int _top;
};// 两个Stack实现队列
class MyQueue
{
public://编译器默认生成MyQueue的构造函数(无参)调⽤了Stack的构造,完成了两个成员的初始化//因此不需要对自定义类型手写构造函数了/*MyQueue() //相当于省略无参构造函数{Stack pushst;Stack popst;}*/
private:Stack pushst;Stack popst;
};
int main()
{//error C2665: “MyQueue::MyQueue”: 没有重载函数可以转换所有参数类型MyQueue mq(4); //MyQueue mq;(编译器自动生成的默认构造函数是无参的)return 0;
}
如果自定义类型的成员变量没有默认构造函数,就要初始化这个成员变量,此时就需要用到初始化列表。
总结:大多数情况,构造函数都需要我们自己去实现。少数情况类似 MyQueue 且 Stack 有默认构造时,MyQueue 自动生成就可以用编译器默认生成的默认构造函数。(因此:构造函数应该写、尽量写)
【补充1】初始化列表
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表。
第一个问题:初始化列表怎样使用?形式是怎样的?
初始化列表的使用方式是:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式:
class Date
{
public://初始化列表Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};
第二个问题:初始化列表有什么用?为什么要使用初始化列表?
每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。(只能初始化一次)
class Date
{
public://初始化列表(每个成员变量定义的地方)Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month), _day(day),_year(1) //error C2437: “_year”: 已初始化{}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private://声明int _year;int _month;int _day;
};int main()
{//对象定义Date d(2025,3,13);d.Print();return 0;
}
总结一下,初始化列表的作用是:定义和初始化每个成员变量。
了解了初始化列表是用来定义成员变量的,那么就引入了三个只能用初始化列表初始化的变量:
- 被 c o n s t const const 修饰的成员变量。
被 c o n s t const const 修饰的变量必须初始化,且初始化后不能再对其修改。
注意: c o n s t const const 修饰的变量(成员变量)只能在初始化定义的时候赋值,其他时候不能对其更改。
//error C2734: “x”: 如果不是外部的,则必须初始化常量对象
const int x;const int y = 1;
//error C3892 : “y”: 不能给常量赋值
y = 2;
因此,如果被 c o n s t const const 修饰的变量是成员变量的话,也必须初始化。所以就不能在函数体内初始化了(普通构造就不能用了),这个时候就只能用初始化列表去定义(必须在定义的时候就初始化)。
- 引用类型( & \& &)的成员变量。
引用类型的变量必须初始化。
//error C2530: “ret”: 必须初始化引用
int& ret;
因此,如果引用类型的变量是成员变量的话,也必须初始化。所以就不能在函数体内初始化了(普通构造就不能用了),这个时候就只能用初始化列表去定义(必须在定义的时候就初始化)。
- 没有默认构造的类类型(自定义类型)的成员变量。
class Time
{
public:Time(int hour) //带参构造函数不是默认构造函数,本来会报错,但是写了初始化列表就没事了:_hour(hour) //hour是int类型,因此也可以在函数体内赋值(等价于下面函数体内注释的内容){cout << "Time()" << endl;//_hour = hour;}void Print(){cout << _hour << endl;}
private:int _hour;
};class Date
{
public://初始化列表Date(int year = 1, int month = 1, int day = 1):_t(12),_y(_year),_x(1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;_t.Print();}
private://声明int _year;int _month;int _day;//必须在初始化列表初始化const int _x; //1.const类型int& _y; //2.引用类型Time _t; //3.自定义类型(没有默认构造)
};int main()
{//对象定义Date d;d.Print();return 0;
}
注意:这里我们看到初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序⽆关。(上面都是逆置的顺序看到了吗?但不影响结果)但是建议声明顺序和初始化列表顺序保持一致。
也就是说,除了这三种情况,其他成员变量既可以在函数内部赋值,也可以在初始化列表初始化,二者有啥区别呢?
这里可以用非成员变量来举例,既然初始化列表是用来定义和初始化成员变量的,那么可以理解为:
//声明(未初始化)
int m;
//函数体内赋值
m = 1;//定义
int n = 1; //初始化列表赋值
看出区别了吗?函数体内赋值相当于先声明后赋值,而初始化列表是直接在定义的时候初始化。
因此,上面三个成员变量不支持先声明,因为定义变量的时候必须要初始化。
C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
什么意思呢?也就是说,可以不光对成员变量进行声明,C++11支持成员变量在声明时给缺省值,使其即使没有初始化列表也能有个保底值:
class Time
{
public:Time(int hour) //带参构造函数不是默认构造函数,本来会报错,但是写了初始化列表就没事了{_hour = hour;}void Print(){cout << _hour << endl;}
private:int _hour;
};class Date
{
public://没写初始化列表也可以(因为有缺省值了)Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;_t.Print();}
private://声明int _year;int _month;int _day;//直接在声明时给缺省值 -> 给初始化列表用的const int _x = 1; //1.const类型int& _y = _year; //2.引用类型Time _t = 12; //3.自定义类型(没有默认构造)
};int main()
{//对象定义Date d;d.Print();return 0;
}
注意:这里是给缺省值而不是定义!(本质上还是声明)—— 因为没有开空间。
虽然这样也可以像这样直接初始化,多方便啊对吧,但是我们还是建议尽量使用初始化列表初始化。
因为那些你不在初始化列表初始化的成员也会走初始化列表。
-
如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。
-
如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。
对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造
会编译错误。
因此,初始化列表的值优先级是最高的,其次是缺省值,再次就是默认构造或者靠编译器了。
初始化列表总结:
-
无论是否显式写初始化列表,每个构造函数都有初始化列表。
-
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化。
【补充2】类型转换
C++ 支持内置类型/类类型隐式类型转换为类类型对象,但需要有相关内置类型/类类型为参数的构造函数。
思考:什么是类型转换?为什么要有类型转换?
class A
{
public://构造函数(为类型转换创建的)A(int a) : _a(a) {}//拷贝构造函数A(const A& a) :_a(a._a) {}void Print(){cout << _a << endl;}int Get() const{return _a;}
private:int _a = 0;
};
class B
{
public://构造函数(为类型转换创建的)B(const A& a) :_b(a.Get()) {}void Print(){cout << _b << endl;}
private:int _b = 0;
};
int main()
{A a1(11); //调用构造cout << "a1:"; a1.Print();A a2 = a1; //调用拷贝构造cout << "a2:"; a2.Print();A a3 = 22; //类型转换(内置类型 -> 临时对象 -> 自定义类型)cout << "a3:"; a3.Print();A a4(33);B b = a4; //类型转换(类类型 -> 临时对象 -> 类类型)cout << "b :"; b.Print();cout << endl;return 0;
}
运行结果为:
这里在定义 a 3 a3 a3 对象时,把右侧内置类型赋值给自定义类型(类型冲突),因此需要进行(隐式)类型转换:
语法上: 构造⼀个 A A A 的临时对象,再用这个临时对象拷贝构造 a 3 a3 a3。
实际上: 编译器遇到 连续构造 + + + 拷贝构造 -> 优化为 直接构造。
补充1:构造函数前面加 e x p l i c i t explicit explicit 就不再支持隐式类型转换:
class A
{
public://加上explicit构造函数就不支持类型转换了explicit A(int a) : _a(a) {}void Print(){cout << _a << endl;}int Get() const{return _a;}
private:int _a = 0;
};int main()
{A a1(123); //普通构造正常运行cout << "a1:"; a1.Print(); //结果为:a1:123//error C2440: “初始化”: 无法从“int”转换为“A”A a2 = 123; //类型转换的构造函数失效了(被explicit修饰了)cout << "a2:"; a2.Print();return 0;
}
类型转换的构造函数失效(被 e x p l i c i t explicit explicit 修饰),运行直接报错:
补充2:C++11后还支持多参数转换:
class C
{
public://构造函数C(int c1 = 10, int c2 = 10) :_c1(c1), _c2(c2) {}void Print(){cout << _c1 + _c2 << endl;}
private:int _c1 = 0;int _c2 = 0;
};int main()
{C c;cout << "普通构造:"; c.Print();//C++11之后才支持多参数类型转换C cc = { 20 , 20 };cout << "类型转换:"; cc.Print();return 0;
}
运行结果为:
2. 析构函数
析构函数与构造函数功能相反。
析构函数( D e s t r u c t o r Destructor Destructor)的主要任务是完成对象中资源的清理释放工作,而不是完成对对象本身的销毁。(局部对象是存在栈帧的,函数结束栈帧销毁就释放了,不需要我们管)
析构函数的本质是要替代我们以前Stack类和Date类中写的Destroy()函数(销毁函数)的功能,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作,其完美的替代了Destroy()函数。
我们会发现 D a t e Date Date 类没有 D e s t r o y ( ) Destroy() Destroy() 函数,其实就是没有资源需要释放(动态开辟的空间和指针变量),所以严格说 D a t e Date Date 类是不需要析构函数的。
因此,我们用 S t a c k Stack Stack 和 M y Q u e u e MyQueue MyQueue 来举例:
typedef int STDataType;class Stack
{
public://构造函数Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc failed!");return;}_capacity = n;_top = 0;}//析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;int _capacity;int _top;
};// 两个Stack实现队列
class MyQueue
{
public://编译器默认⽣成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源//显式写析构,也会自动调用Stack的析构~MyQueue(){cout << "~MyQueue()" << endl;}
private:Stack pushst;Stack popst;
};int main()
{Stack st; //析构一次 Stack stMyQueue mq; //析构两次 Stack pushst、Stack popstreturn 0;
}
析构函数的特点:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值。 (这里跟构造类似,也不需要加 v o i d void void)
-
对象生命周期结束时,系统会自动调用析构函数。
-
一个类只能有一个析构函数。(若未显式定义,系统会自动生成默认的析构函数)
-
编译器自动生成的析构函数对内置类型成员不做处理。
-
自定义类型成员无论什么情况都会自动调用析构函数。
如上面写的 M y Q u e u e MyQueue MyQueue 类,运行结果如下:
因此对于自定义类型的成员变量不管写没写析构,都会自动调用成员变量的析构函数。
如果类中没有申请资源或者默认生成的析构可以用时,析构函数可以不写,直接使用编译器生成的默认析构函数即可,如 D a t e Date Date 和 M y Q u e u e MyQueue MyQueue;
如果有资源申请时,一定要自己写析构,否则会造成资源泄漏,如 S t a c k Stack Stack。
- 一个局部域的多个对象,C++规定后定义的先析构。(栈帧结构:后进先出)
3. 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说:拷贝构造是一个特殊的构造函数。
还是以日期类来举例:
class Date
{
public://构造函数Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷贝构造函数Date(const Date& d) //将d1的值拷贝给d2{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025,3,10); //普通构造d1.Print();Date d2(d1); //拷贝构造d2.Print();return 0;
}
运行结果为:
可以看出拷贝构造其实是用对象去初始化对象(这里是用 d 1 d1 d1 初始化了 d 2 d2 d2)。
拷贝构造的特点:
-
拷贝构造函数是构造函数的一个重载。
-
拷贝构造函数的第一个参数必须是类类型对象的引用,如果有多个参数,后面的参数必须有缺省值。
使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。
- C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
class Date
{
public://构造函数Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷贝构造函数Date(const Date& d){cout << "拷贝构造" << endl;_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};void Func1(Date d2)
{d2.Print();
}int main()
{Date d1(2025,3,10);//对象拷贝要要调用拷贝构造Date d2(d1);//传值传参要调用拷贝构造Func1(d2); //调用 Date(const Date& d) 函数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;int _month;int _day;
};void Func1(Date d2)
{d2.Print();
}int main()
{Date d1(2025,3,10);//对象拷贝要要调用拷贝构造Date d2(d1);//传值传参要调用拷贝构造Func1(d2); //调用(编译器自动生成的)拷贝构造函数return 0;
}
运行结果:
编译器自动生成的拷贝构造函数就已经很好地完成了任务,因此就没有必要再自己写拷贝构造了。
因此总结一下:
- 像 D a t e Date Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要拷贝(浅拷贝),所以不需要我们显式实现拷贝构造。
浅拷贝:一个字节一个字节地拷贝。
- 而像 S t a c k Stack Stack 这样的类,虽然也都是内置类型,但是 _ a \_a _a 指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
深拷贝:对指向的资源也进行拷贝。
typedef int STDataType;class Stack
{
public://构造函数Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc failed!");return;}_capacity = n;_top = 0;}//析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;} void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}//没有写拷贝构造 -> 调用编译器自己生成的(浅拷贝)
private:STDataType* _a;int _capacity;int _top;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);// Stack不显式实现拷贝构造,用自动生成的拷贝构造完成浅拷贝// 会导致st1和st2⾥⾯的_a指针指向同一块资源,析构时会析构两次,程序崩溃Stack st2 = st1;return 0;
}
因为编译器自己生成的拷贝构造时直接拷贝(浅拷贝),会导致 s t 1 st1 st1 和 s t 2 st2 st2 里的 _ a \_a _a 指针指向同一块空间,析构时会析构两次( f r e e free free 了两次),程序直接就崩溃了:
这时候会发现,把析构函数 ~ S t a c k ( ) Stack() Stack() 删了就不会报错了。(但是这样会内存泄漏啊!!!不能这样干)
因此我们这个时候就必须自己写拷贝构造函数了:
typedef int STDataType;class Stack
{
public://构造函数Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc failed!");return;}_capacity = n;_top = 0;}//析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;} void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}//拷贝构造函数Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc failed!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}
private:STDataType* _a;int _capacity;int _top;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);Stack st2(st1); //Stack st2 = st1;return 0;
}
运行结果:
这里可以看到程序正常运行没有崩溃,且析构函数调用了两次(说明两个 _ a \_a _a 指针没有指向同一块空间)。因此只有自己写的拷贝构造函数才能避免指针指向同一块空间。
- 像 M y Q u e u e MyQueue MyQueue 这样的类型内部主要是自定义类型 S t a c k Stack Stack 成员,编译器自动生成的拷贝构造会调用 S t a c k Stack Stack 的拷贝构造,也不需要我们显式地实现 M y Q u e u e MyQueue MyQueue 的拷贝构造。
typedef int STDataType;class Stack
{
public://构造函数Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc failed!");return;}_capacity = n;_top = 0;}//析构函数~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;} void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail!");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}//拷贝构造函数Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc failed!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}
private:STDataType* _a;int _capacity;int _top;
};// 两个Stack实现队列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};void func(MyQueue mq)
{//...
}int main()
{MyQueue mq; // 调用MyQueue的构造函数(编译器实现) -> 分别调用两个Stack的构造函数func(mq); // 调用MyQueue的拷贝构造(编译器实现) -> 分别调用两个Stack的拷贝构造// 调用MyQueue的析构函数(编译器实现) -> 分别调用两个Stack的析构函数// (析构四次:两个构造的Stack和两个拷贝构造的Stack)return 0;
}
运行结果为:
一共析构了四次:两个构造的 S t a c k Stack Stack 和两个拷贝构造的 S t a c k Stack Stack。
这里有一个小技巧:如果一个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则就不需要。
P.S. 拷贝构造可以写成两种形式:
1. Stack st2(st1);
2. Stack st2 = st1;
这两种写法是等价的,都代表调用st1的拷贝构造函数,将结果拷贝给st2。
-
传值返回会产生一个临时对象调用拷贝构造。
传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。
Stack func()
{Stack st;return st; //调用Stack的拷贝构造
}int main()
{Stack ret = func(); //将拷贝构造的值返回给retreturn 0;
}
( 注意: 如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样 )
Stack& func()
{Stack st; //函数内部定义的局部变量出函数就会被销毁//warning C4172: 返回局部变量的地址或临时 : streturn st; //返回st被销毁因此返回了一个野引用
}int main()
{Stack ret = func(); //此时ret为野引用(一旦调用就会报错)ret.Push(1); //realloc failed!return 0;
}
运行结果:
这里因为对野引用进行操作所以就直接报错了。因此我们传引用返回的时候,要保证对象没有被销毁。
因此我们可以在函数内部定义的对象加上 s t a t i c static static 让他变为静态全局的,延长它的生命周期;还可以把对象在函数外定义好了传参给函数,这样就不怕出了函数作用域对象销毁的问题了。
传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,(保证不是野引用)才能用引用返回。
4. 赋值运算符重载
4.1 运算符重载
当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。
运算符重载是一个特殊的函数,他的名字是由 o p e r a t o r operator operator 和后面要定义的运算符共同构成。
( 返回类型 ) o p e r a t o r ( 运算符 ) ( 参数 1 , 参数 2 ) { 函数体 } (返回类型)\ operator(运算符)\ (参数1,参数2) \{函数体\} (返回类型) operator(运算符) (参数1,参数2){函数体}
- C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
class Date
{//...};bool operator == (Date d1, Date d2)
{//...}int main()
{Date d1, d2;//如果没有定义operator函数会报错://error C2676: 二进制“==”:“Date”不定义该运算符或到预定义运算符可接收的类型的转换d1 == d2; //如果定义了operator==()函数会自动转化成下面的形式operator == (d1,d2);return 0;
}
-
一元运算符有一个参数、二元运算符有两个参数(左侧运算对象传给第一个参数,右侧运算对象传给第二个参数) . . . ... ... n n n 元运算符有 n n n 个参数。(大部分都是一元和二元)
-
如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 t h i s this this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个(实际上少的那一个变成隐含的了)。
如果全局和成员都写了运算符重载,会优先调用成员函数的运算符重载。(先找类域,再找全局)
当 o p e r a t o r operator operator 定义为全局函数时,由于类的成员变量都是私有的,因此 o p e r a t o r operator operator 不能直接访问其成员变量:
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;
};bool operator == (Date d1, Date d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1, d2(2025);// operator == (d1,d2)if (d1 == d2)cout << "等于" << endl;elsecout << "不等于" << endl;return 0;
}
这里是把是直接把 p r i v a t e : private: private: 给注释掉了,即把成员变量先置为公有,让 o p e r a t o r operator operator 能够访问到,不然会直接报错: error C2248: “Date::_year”: 无法访问 private 成员(在“Date”类中声明)
。当然也可以像 J a v a Java Java 一样搞一个 G e t ( ) Get() Get() 函数,让成员变量变成只读。
但是我们还可以直接把 o p e r a t o r operator operator 函数直接放到类内部,这样就能直接访问:
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;}bool operator == (Date d1, Date d2){return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1, d2(2025);//error C2804: 二进制“operator ==”的参数太多if (d1 == d2) cout << "等于" << endl;elsecout << "不等于" << endl;return 0;
}
这里直接把 o p e r a t o r operator operator 函数复制粘贴到类里面,运行后会发现直接报错了:error C2804: 二进制“operator ==”的参数太多
。
之前我们在学习 t h i s this this 指针的时候,知道成员函数默认都会在形参第一个位置,增加一个当前类类型的指针 Date* const this
,叫做 t h i s this this 指针。
因此, o p e r a t o r operator operator 做成员变量的时候,第一个参数会默认为当前对象的 t h i s this this 指针(隐藏起来了),这个指针访问的是当前类的成员变量即第一个对象的成员变量,这时只需要传一个参数(代表右侧运算对象)即可。
总结:运算符重载作为成员函数时,参数比运算对象少一个。(第一个参数为 t h i s this this 指针)
因此修改后的 o p e r a t o r = = ( ) operator == () operator==() 函数应该只有一个参数:
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;}bool operator == (Date d){return _year == d._year&& _month == d._month&& _day == d._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1, d2(2025);// d1.operator == (d2)if (d1 == d2)cout << "等于" << endl;elsecout << "不等于" << endl;return 0;
}
归纳总结:重载为全局的面临对象访问私有成员变量的问题,有几种方法可以解决:
成员放公有。
D a t e Date Date 提供 g e t x x x getxxx getxxx 函数。
友元函数。
重载为成员函数。
-
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
-
不能通过连接语法中没有的符号来创建新的操作符:比如 o p e r a t o r operator operator @。
注意:以下五个运算符不能重载:(1) . ∗ .* .∗ (2) : : :: :: (3) s i z e o f sizeof sizeof (4) ? : ?: ?: (5) . . .(面试题常考)。
-
重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x, int y)
。 -
一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如 D a t e Date Date 类重载 o p e r a t o r − operator- operator− 就有意义(中间多少天),但是重载 o p e r a t o r + operator+ operator+ 就没有意义。
-
重载 + + ++ ++ 运算符时,有 前置++ 和 后置++,运算符重载函数名都是 o p e r a t o r + + operator++ operator++,无法很好的区分。
C++规定:后置++重载时,增加⼀个 i n t int int 形参,跟前置++构成函数重载,方便区分。
//前置++/--
Date& operator ++ (); //++d
Date& operator -- (); //--d//后置++/--
//Date operator ++ (0);
Date operator ++ (int); //d++
//Date operator ++ (0);
Date operator -- (int); //d--
- 重载 < < << << 和 > > >> >> 时,需要重载为全局函数。(因为重载为成员函数, t h i s this this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象 < < c o u t 对象<<cout 对象<<cout,不符合使用习惯和可读性)
class Date
{
public://...void Date::operator << (ostream& out){out << _year << "年" << _month << "月" << _day << "日" << endl;}
private://...
};
int main()
{Date d(2025,3,12);//error C2679: 二元“<<”: 没有找到接受“Date”类型的右操作数的运算符(或没有可接受的转换)//cout << d;d << cout;d.operator << (cout);return 0;
}
如果写成 c o u t < < d cout << d cout<<d 会直接报错,因为参数位置不匹配:
因为成员函数的第一个参数永远是 t h i s this this 指针,因此会把 c o u t cout cout 挤到第二个参数去。
于是写成 d < < c o u t d << cout d<<cout 就不会报错了:
写成这样虽然正确,但用起来就有点倒反天罡的意思了。
因此,我们可以重载为全局函数,把 o s t r e a m / i s t r e a m ostream/istream ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象:
注意:流对象不支持拷贝构造,因此调用时必须用引用:比如调用 c o u t cout cout 时用 o s t r e a m & ostream\& ostream&。
class Date
{//友元函数声明friend void operator << (ostream& out, const Date& d);
public://...
private://...
};//定义在全局(保证了参数顺序)
void operator << (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}int main()
{Date d(2025,3,12);cout << d;operator << (cout, d)return 0;
}
这样就可以正常运行了:
注意: o p e r a t o r < < operator<< operator<< 在重载为全局函数之后,便对 D a t e Date Date 的成员变量失去了访问权(私有成员)。因此在 D a t e Date Date 类中对 o p e r a t o r < < operator<< operator<< 函数加上一个友元声明。
这个时候还有一个问题,怎么解决连续输出的问题呢?—— 答案是:可以增加一个返回值 c o u t cout cout。
再更改一下上面的程序,整理一下就得到了一个比较完善的流提取的运算符重载函数:
class Date
{//友元函数声明friend ostream& operator << (ostream& out, const Date& d); //输出
public://...
private://...
};//定义在全局(保证了参数顺序)
ostream& operator << (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}int main()
{Date d1(2025,3,12), d2(2005,8,23);cout << d1 << d2 << endl;operator << (operator << (cout, d1), d2);return 0;
}
运行结果如下,完美地完成了任务:
注意:赋值运算符是从右往左赋值,而流提取是从左往右返回。因此赋值操作的返回值会给第二个参数,而流提取的返回值会给第一个参数。
4.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值。(这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象)
还是以日期类来举例:
Date d1(2025,3,10);
Date d2(2025,3,11);//Date d3(d1);
Date d3 = d1; //拷贝构造(初始化还未创建的对象d1)d1 = d2; //赋值重载(两个已经存在的对象d1、d2)
赋值运算符重载的特点:
-
赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 c o n s t const const 当前类类型引用,否则会传值传参会有拷贝。
-
有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
int i, j, k;//赋值表达式的返回值是:左操作数
i = j = k = 1; //对内置类型变量进行连续赋值
// k = 1(返回k)-> j = k(返回j)-> i = j Date d1, d2, d3;d1 = d2 = d3; //对自定义类型进行连续赋值
//同上:d2 = d3(返回d2)-> d1 = d2
//即:d1 = (operator = (d2,d3)) -> operator = (d1,operator = (d2,d3.))
这里的 o p e r a t o r = operator= operator= 是全局函数,成员函数的 o p e r a t o r = operator= operator= 第一个参数是 t h i s this this 指针,应该返回啥呢?
class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}const Date& operator = (const Date& d) //返回值和参数都建议写成引用(提高效率){_year = d._year;_month = d._month;_day = d._day;//this指针指向的是左侧运算对象的地址return *this; //解引用后代表左侧运算对象}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1, d2(2025,3,12), d3(2005, 8, 23);cout << "d1:"; d1.Print();cout << "d2:"; d2.Print();cout << "d3:"; d3.Print();d1 = d2 = d3; // (d1 = (d2 = d3));// (d2 = d3)的返回值是d2,(d1 = d2)的返回值是d1cout << endl;cout << "d1:"; d1.Print();cout << "d2:"; d2.Print();cout << "d3:"; d3.Print();return 0;
}
运行结果为:
可以看出,赋值重载给了返回值就可以进行连续赋值操作。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
这里和拷贝构造非常像:
- 像 D a t e Date Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝(浅拷贝),所以不需要我们显式实现赋值运算符重载。
- 像 S t a c k Stack Stack 这样的类,虽然也都是内置类型,但是 _ a \_a _a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
- 像 M y Q u e u e MyQueue MyQueue 这样的类型内部主要是自定义类型 S t a c k Stack Stack 成员,编译器自动生成的赋值运算符重载会调用 S t a c k Stack Stack 的赋值运算符重载,也不需要我们显式实现 M y Q u e u e MyQueue MyQueue 的赋值运算符重载。
这里还有一个小技巧:如果一个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载,否则就不需要。
5. 取地址运算符重载
我们在定义对象的时候,不仅会定义普通对象,也可能会定义 c o n s t const const 对象。
以日期类来举例:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// void Print(Date* const this)void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025,3,13);d1.Print(); //传的值&d1是Date*const Date d2(2025,4,12);//error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”d2.Print(); //传的值&d2是Date*(应该传const Date*)return 0;
}
这里如果定义的 c o n s t const const 对象 d 2 d2 d2 如果想要访问其成员函数的话,会造成权限放大问题:
这里的 d 2 d2 d2 传的 t h i s this this 指针是 D a t e ∗ Date^* Date∗ 类型的(忘了的话可以翻翻上面第三章关于 t h i s this this 指针的内容),因此在语法层面上来讲,在成员函数里并没有对其限定不可修改,如果我想修改也是可以修改的:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// void Print(const Date* const this)void Print(){this->_year = 0; this->_month = 0; this->_day = 0; //直接修改cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d(2025, 3, 13);d.Print(); //传的是 &d -> Date* const thisreturn 0;
}
注意:这里Date* const this里的const是指定this不能修改,而不是指定this所指向的空间(Date类)不能修改。
完全可以把对象信息给改了:
所以,如果我们定义的对象是 c o n s t const const 对象,那么传到成员函数的参数 t h i s this this 指针却是 D a t e ∗ c o n s t t h i s Date^*\ const\ this Date∗ const this ,我们理应应当把其改为 c o n s t D a t e ∗ c o n s t t h i s const\ Date^*\ const\ this const Date∗ const this 类型,才能确保对象受 c o n s t const const 保护,在函数中也就不能随便修改了。
那么怎么在成员函数加上 c o n s t const const 呢?在哪加呢?—— 我们引入了 c o n s t const const 成员函数。
5.1 const 成员函数
将 c o n s t const const 修饰的成员函数称之为 c o n s t const const 成员函数, c o n s t const const 修饰成员函数放到成员函数参数列表的后面 。
以 D a t e Date Date 类里的 P r i n t ( ) Print() Print() 函数举例:
c o n s t const const 修饰 D a t e Date Date 类的 P r i n t ( ) Print() Print() 成员函数, P r i n t ( ) Print() Print() 隐含的 t h i s this this 指针由 D a t e ∗ c o n s t t h i s Date^*\ const\ this Date∗ const this 要变为 c o n s t D a t e ∗ c o n s t t h i s const\ Date^*\ const\ this const Date∗ const this 才能保证对象不被修改(权限匹配),而由于 t h i s this this 指针是隐含的,没有办法直接在参数前加上 c o n s t const const。
因此,规定成员函数 v o i d P r i n t ( ) { . . . } void\ Print()\ \{...\} void Print() {...} 用 c o n s t const const 修饰后写成 v o i d P r i n t ( ) c o n s t { . . . } void\ Print()\ const\ \{...\} void Print() const {...}。
c o n s t const const 实际修饰该成员函数隐含的 t h i s this this 指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// void Print(const Date* const this)void Print() const{//error C3490 : 由于正在通过常量对象访问“_year”,因此无法对其进行修改//error C3490 : 由于正在通过常量对象访问“_month”,因此无法对其进行修改//error C3490: 由于正在通过常量对象访问“_day”,因此无法对其进行修改//this->_year = 0; this->_month = 0; this->_day = 0;cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{// 这里非const对象也可以调用const成员函数是一种权限的缩小Date d1(2025, 3, 13);d1.Print();const Date d2(2025, 4, 12);d2.Print();return 0;
}
这样就成功调用 d 2 d2 d2 :
且如果修改了对象的内容会直接报错:
总结:只要不修改对象的成员函数都可以加 c o n s t const const ,加上 c o n s t const const 肯定更安全。
5.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和 c o n s t const const 取地址运算符重载。
一般这两个函数编译器自动生成的就可以够我们用了,不需要我们去显式实现。(默认成员函数)
除非一些很特殊的场景:比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。(可见也没啥真正使实用的应用场景)
class Date
{
public://1.普通取地址Date* operator & (){return this;}//2.const取地址const Date* operator & () const{return this;}
private:int _year;int _month;int _day;
};
可见这两个函数唯一的区别在于返回值:
- 普通取地址运算符重载:需要返回 D a t e ∗ Date^* Date∗。
- c o n s t const const 取地址运算符重载:需要返回 c o n s t D a t e ∗ const\ Date^* const Date∗
注意:虽然普通对象也可以访问const取地址重载函数,但返回的是const Date*就不合理,因此要写两个。
函数实现直接返回 t h i s this this 指针(本身就是指向对象的地址)即可,根本不用写什么东西,编译器都实现好了。
五、static 成员
⽤ s t a t i c static static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。(但是要在类内声明:因为静态成员也是类的成员,因此也受 p u b l i c public public、 p r o t e c t e d protected protected、 p r i v a t e private private 访问限定符的限制)
注意:这里静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
注意:静态成员变量为类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
class A
{
public://...
private:static int _a;
};int main()
{A a;cout << sizeof(a) << endl;return 0;
}
运行结果为:
可以看出,静态成员变量并没有存到对象里。(这里对象大小为 1 1 1 说明对象里面没有值)
⽤ s t a t i c static static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 t h i s this this 指针。
因此,
-
静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有 t h i s this this 指针。
-
非静态的成员函数,可以访问任意的静态成员变量和非静态成员函数,因为有 t h i s this this 指针。
也就是说,静态成员是大家公有的、共享的;而非静态成员只能通过 t h i s this this 指针来访问。
突破类域就可以访问静态成员:
可以通过 类名 : : 静态成员 类名::静态成员 类名::静态成员 或者 对象 . 静态成员 对象.静态成员 对象.静态成员来访问静态成员变量和静态成员函数。
class A
{
public://...static int Get_a1(){return _a1;//error C2597: 对非静态成员“A::a1”的非法引用//return _a2; //静态成员函数不能访问非静态成员变量}void Print(){cout << _a1 << " " << _a2 << endl; //非静态成员函数可以随便访问任意成员变量}
private://类内部声明static int _a1;int _a2 = 1;
};//类外部初始化
int A::_a1 = 10; int main()
{A a;//打印_a的值:10cout << A::Get_a1() << endl; //1.类名::静态成员cout << a.Get_a1() << endl; //2.对象.静态成员return 0;
}
那么 s t a t i c static static 成员具体有啥应用呢?
提出一个问题:实现一个类,计算程序中创建出了多少个类对象?
class A
{
public://构造函数A(){_scount++;}//拷贝构造函数A(const A& a){_scount++; }//析构函数~A(){_scount--;}int Get_scount(){return _scount;}
private://类里面声明static int _scount; //计算程序创建了多少个类对象
}; //(构造+1、析构-1)//类外面初始化
int A::_scount = 0;int main()
{A a[10]; //创建了10个A类型对象//由此可见,10个对象的static成员都是共享的for(int i = 0; i < 10; i++)cout << a[i].Get_scount() << " ";cout << endl;return 0;
}
运行结果为:
由此可见, s t a t i c static static 成员确实都是共享的,每个对象都一样。
通过上面这段程序,我们就可以用这种方法做下面这道题:
【题目信息】
【剑指 o f f e r offer offer】 64. 64. 64. 求 1 + 2 + 3 + . . . + n 1+2+3+...+n 1+2+3+...+n :
这是 《剑指 o f f e r offer offer》 上的一道题,虽然题目很简单,但是给了很多条件限制,因此常规方法基本上是不能用的,所以我们要另辟蹊径 —— 用非常规方法:
【题目解析】
这道题的意思是只能用 + 、 − +、- +、− 来实现累加了,那么找规律发现一共有 n n n 项,从第 1 1 1 项到第 n n n 项,每一项都是上一项 + + ++ ++ 后的结果,也就是说可以定义一个 i i i 变量代表每一个元素,加到一个初始值为 0 0 0 的 s u m sum sum 变量中,代表累加和,每次加完就让 i + + i++ i++ ,一共加上 n n n 次就完美的解决了问题。
现在的问题是:怎么不用循环累加 n n n 次呢?
这看似有些难为人,但可以发现,我们上面的程序可以计算程序中创建出了多少个类对象,也就是说,创建多少次对象,就会执行多少次构造函数。
因此,我们可以将 i i i 和 s u m sum sum 定义成静态变量,放到构造函数里去累加,这样如果创建一个个数为 n n n 的 S u m Sum Sum 类类型数组,即创建 n n n 个对象,那么这个构造函数就会被执行 n n n 次,这样就很好的完成了任务。
【代码实现】
class Sum
{
public:Sum(){_i++; //_i从0自增到n-1_ret+=_i; //_ret(n)从1加到n-1}static int Get_ret(){return _ret;}static int _i;static int _ret;
};int Sum::_ret = 0;
int Sum::_i = 0;class Solution {
public:int Sum_Solution(int n) {Sum s[n]; //变长数组return Sum::Get_ret();}
};
【题目总结】
虽然题目做出来了,但是这也仅仅适用于面试这种专门难为人的应用场景了,可以说在现实中根本不会这样用,也就是说这种方法只有教学意义,而没有实际应用意义。
六、友元
友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 f r i e n d friend friend,并且把友元声明放到一个类的里面。
1. 友元函数:
外部友元函数可访问类的私有( p r i v a t e private private)和保护( p r o t e c t e d protected protected)成员,友元函数仅仅是一种声明,他不是类的成员函数。
class A
{//这里可以看出友元函数可以在类定义的任何地方声明,不受类访问限定符限制。friend void Print_fa(const A& a); //友元声明
public://...
private:int _a = 123;
};//1.未声明友元
void Print_a(const A& a)
{//error C2248: “A::_a”: 无法访问 private 成员(在“A”类中声明)cout << a._a << endl;
}//2.声明友元
void Print_fa(const A& a)
{cout << a._a << endl;
}int main()
{A a;Print_fa(a);return 0;
}
注意:这里可以看出,友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
可以说,友元存在的目的就是为了让类外函数能够访问类内的成员。
一个函数还可以是多个类的友元函数。
也就是说,一个函数可以访问到多个类的成员,这也很合理。
// 前置声明,否则A的友元函数声明编译器不认识B
class B;class A
{//友元声明friend void Print_f(const A& a, const B& b);
public://...
private:int _a = 123;
};class B
{//友元声明friend void Print_f(const A& a, const B& b);
public://...
private:int _b = 456;
};void Print_f(const A& a,const B& b)
{cout << a._a << " " << b._b << endl;
}int main()
{A a; B b;Print_f(a,b);return 0;
}
注意:一定要加前置声明class B,否则A的友元函数声明编译器不认识B,会直接报错。
2. 友元类:
友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
所以,要是一个类需要频繁访问另一个类的非公有成员的话,声明成友元类比较方便:
class A
{//友元声明friend class B; //即类B可以访问A的所有成员了
public://...
private:int _a1 = 123;int _a2 = 456;
};class B
{
public:void func1(const A& a){cout << "a1:" << a._a1 << endl;}void func2(const A& a){cout << "a2:" << a._a2 << endl;}void func3(const A& a){cout << "a1 + a2:" << a._a1 + a._a2 << endl;}
private://...
};int main()
{A a; B b;b.func1(a);b.func2(a);cout << endl;b.func3(a);return 0;
}
友元类的关系是单向的,不具有交换性,比如 A A A 类是 B B B 类的友元,但是 B B B 类不是 A A A 类的友元。
class A
{//友元声明friend class B; //即类B可以访问A的所有成员了,但是A不能访问B的成员
public:void func(const B& b){//error C2027: 使用了未定义类型“B”cout << "b:" << b._b << endl;}
private:int _a = 123;
};class B
{
public:void func(const A& a){cout << "a:" << a._a << endl;}
private:int _b = 456;
};int main()
{A a; B b;a.func(b); //报错:成员 "B::_b" (已声明) 不可访问b.func(a); //a:123return 0;
}
友元类关系不能传递,如果 A A A 是 B B B 的友元, B B B 是 C C C 的友元,但是 A A A 不是 C C C 的友元。
class A
{//友元声明friend class B; //A是B的友元
public://...
private:int _a = 123;
};class B
{//友元声明friend class C; //B是C的友元
public:void Print(const A& a){cout << a._a << endl;}
private:int _b = 456;
};class C
{
public:void Print(const B& b){cout << b._b << endl;}void Print(const A& a){cout << a._a << endl;}
private:int _c = 789;
};int main()
{A a; B b; C c;b.Print(a); //A是B的友元c.Print(b); //B是C的友元//error C2248: “A::_a”: 无法访问 private 成员(在“A”类中声明)c.Print(a); //推不出A是C的友元(因此C不能访问A)return 0;
}
友元总结:
虽然友元有时提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
七、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
class A
{
private:int _a = 123;
public:class B //B默认为A的友元{public://...private:int _b = 456;};
};int main()
{A a;cout << sizeof(a) << endl;return 0;
}
运行结果为:
A A A 类对象的大小为 4 4 4,可以看出 A A A 类中的成员只包含了 _ a \_a _a 这一个成员变量,并没有包含内部类 B B B。
也就是说,内部类不是外部类的成员,定义在外部类的内部只是因为内部类默认是外部类的友元类。
class A
{//内部类默认为友元//friend class B;
private:int _a = 123;static int _sa;
public:class B //A默认为B的友元(但B不是A的友元){//A如果想要访问B的成员,必须要进行友元声明friend class A;public:void Print(const A& a){cout << a._a << " " << a._sa << endl;}private:int _b = 456;};void Print(const B& b){//不加友元声明会报错:error C2248: “A::B::_b”: 无法访问 private 成员(在“A::B”类中声明)cout << b._b << endl;}
};int A::_sa = 789;int main()
{A a; A::B b; //指定类域b.Print(a); //内部类默认能够访问外部类a.Print(b); //外部类要加友元声明才能访问内部类return 0;
}
总结一下:内部类本质也是一种封装。当 A A A 类跟 B B B 类紧密关联, A A A 类实现出来主要就是给 B B B 类使用,那么可以考虑把 A A A 类设计为 B B B 的内部类。(如果放到 p r i v a t e / p r o t e c t e d private/protected private/protected 位置,那么 A A A 类就是 B B B 类的专属内部类,其他地方都用不了)
八、匿名对象
用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。
class A
{
public:A(int a = 123){_a = a;}void Print(){cout << _a << endl;}
private:int _a;
};int main()
{A a1(456); //有名对象a1.Print();//error C2228: “.Print”的左边必须有类/结构/联合(说明了编译器没有把a2对象定义出来)A a2(); //不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义//a2.Print();A(); //匿名对象A(789);return 0;
}
那么我们就有问题为什么要定义匿名对象呢?匿名对象可以用来干什么呢?
匿名对象的生命周期只在当前一行,一般如果想临时定义一个对象就当前用一下即可,如:想直接调用成员函数,就可以定义匿名对象,这样就不必为了调用类中的函数而特意定义一个对象了:
class Solution
{
public:void Print(){cout << "Solution()" << endl;}
private://...
};int main()
{//先定义有名对象,再调用其成员函数Solution st; //生命周期在整个域内(main函数)st.Print();//直接调用匿名对象的成员函数(这一行代码结束,匿名对象就销毁了)Solution().Print(); //生命周期在这一行return 0;
}
九、对象拷贝时编译器的优化(了解)
现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
具体如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。
当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
L i n u x Linux Linux下可以将下面代码拷贝到 t e s t . c p p test.cpp test.cpp 文件,编译时用
g++ test.cpp -fno-elide-constructors
的方式关闭构造相关的优化。
class A
{
public://构造函数A(int a = 0):_a(a){cout << "A(int a)" << endl;}//拷贝构造函数A(const A& a):_a(a._a){cout << "A(const A& a)" << endl;}//赋值重载函数A& operator = (const A& a){cout << "A& operator = (cosnt A& a)" << endl;if (this != &a){_a = a._a;}return *this;}//析构函数~A(){cout << "~A()" << endl;}
private:int _a = 1;
};void func1(A a)
{}void func2(const A& a) //引用传参可以减少拷贝(不用进行一次拷贝构造了)
{}A func3()
{A a; //构造1次return a; //a出了作用域就销毁了,因此返回时会产生临时对象(拷贝构造1次)
}int main()
{//类型转换本来应该先产生临时变量(构造1次),再将临时变量拷贝给类类型变量(拷贝构造1次)//但这里只构造了一次,是因为编译器进行了合并优化A a1 = 1; // A(int a)A a2 = 1;//传值传参(拷贝)func1(a2); // A(int a) + A(const A& a)//引用传参func2(a2); // A(int a)//匿名对象(优化)func1(A(1)); // A(int a)//隐式类型转换(临时构造)-> 但编译器优化了(直接构造)func1(1); // A(int a)//编译器vs2022都优化了(把返回的临时变量a直接优化掉了)func3(); // A(int a)func3().Print(); // A(int a)A ret = func3(); // A(int a)A ret; // A(int a)ret = func3(); // A(int a) + A& operator = (cosnt A& a)return 0;
}
这部分只做了解即可,因为不同的编译器可能优化方式不同,因为C++标准没有明确统一规定,因此我们只要做到见怪不怪即可。
完整代码示例
1. C++ 和 C 语言实现 Stack 对比(封装)
面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解⼀下封装。
C++实现 S t a c k Stack Stack 形态上还是发生了挺多的变化,底层和逻辑上没啥变化。(只是进行了封装)
- C语言:
/*1.栈的数据结构*/typedef int STDataType;
//支持动态增长的栈
typedef struct Stack
{STDataType* a;int top; //栈顶int capacity; //容量
}ST;/*2.栈的基本操作*///1.初始化
void STInit(ST* ps)
{assert(ps);ps->a = NULL;ps->capacity = 0;//1.top指向栈顶的下一个元素
#if 1ps->top = 0;
#endif//2.top指向栈顶元素
#if 0ps->top = -1;
#endif
}//2.入栈
void STPush(ST* ps, STDataType x)
{assert(ps);//判断是否需要扩容if (ps->capacity == ps->top){int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));//判断realloc是否成功申请空间if (tmp == NULL){perror("relloc failed!");return;}ps->a = tmp;ps->capacity = newcapacity;}//插入元素ps->a[ps->top] = x;ps->top++;
}//3.出栈
void STPop(ST* ps)
{assert(ps && ps->top > 0);//top=0时栈中恰好没有元素(NULL)ps->top--;
}//4.获取栈顶元素
STDataType STTop(ST* ps)
{assert(ps && ps->top > 0);return ps->a[ps->top - 1];
}//5.获取栈中有效元素的个数
int STSize(ST* ps)
{assert(ps);return ps->top;
}//6.检测栈是否为空
bool STEmpty(ST* ps)
{assert(ps);if (ps->top > 0)return false;return true;
}//7.销毁栈
void STDestroy(ST* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->capacity = ps->top = 0;
}//测试
int main()
{ST s;STInit(&s);STPush(&s, 1);STPush(&s, 2);STPush(&s, 3);STPush(&s, 4);while (!STEmpty(&s)){printf("%d\n", STTop(&s));STPop(&s);}STDestroy(&s);return 0;
}
- C++:
typedef int STDataType;class Stack
{
public://成员函数//1.初始化void Init(int n = 4) {_a = (STDataType*)malloc(sizeof(STDataType) * n);if (_a == nullptr){perror("malloc failed!");return;}_top = 0;_capacity = n;}//2.入栈void Push(STDataType x){//判断是否需要扩容if (_capacity == _top){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));//判断realloc是否成功申请空间if (tmp == nullptr){perror("relloc failed!");return;}_a = tmp;_capacity = newcapacity;}//插入元素_a[_top++] = x;}//3.出栈void Pop(){assert(_top > 0); //top=0时栈中恰好没有元素_top--;}//4.获取栈顶元素STDataType Top(){assert(_top > 0);return _a[_top - 1];}//5.获取栈中有效元素的个数int Size(){return _top;}//6.检测栈是否为空bool Empty(){return _top == 0;}//7.销毁栈void Destroy(){free(_a);_a = nullptr;_capacity = _top = 0;}private://成员变量STDataType* _a;int _top; //栈顶int _capacity; //容量
};//测试
int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);while (!s.Empty()){cout << s.Top() << endl;s.Pop();}s.Destroy();return 0;
}
C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。
上述C++实现栈的代码相对于C语言可以说简洁了许多,主要有以下改变:
- I n i t ( ) Init() Init() 函数给的缺省参数会方便很多。
- 成员函数每次不需要传对象地址,因为 t h i s this this 指针隐含的传递了,方便了很多。
- 使用类型不再需要 t y p e d e f typedef typedef,直接用类名就很方便。
这只是 C++ 入门阶段实现的 S t a c k Stack Stack 看起来变了很多,但是实质上变化不大。等后面看 S T L STL STL 中的用适配器实现的 S t a c k Stack Stack,就能够感受到 C++ 的魅力。
2. 日期类的实现(运算符重载)
实现一个日期类主要是为了练习运算符重载的使用,使一个自定义类型能够进行和内置类型类似的加、减、比较大小和输入输出等运算符操作,同时也能学会日期类问题的解决思路。
我们在实现一个项目(类)时,要做到声明和定义分离,养成一个好习惯,因此我们把日期类总共分为了三个文件:
- d a t e . h date.h date.h 用来存放头文件以及日期类的定义和其方法的声明;
#pragma once
#include<iostream>
#include<assert.h>using namespace std;class Date
{//友元函数声明friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);public://检查非法日期bool CheckDate() const;//全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1);//析构函数~Date();//拷贝构造函数 d2(d1)Date(const Date& d);//赋值运算符重载 d2 = d3 -> d2.operator=(&d2, d3)Date& operator = (const Date& d);//打印日期void Print() const;//获取某年某月的天数(默认是inline)inline int GetMonthDay(int year, int month) const{assert(month > 0 && month < 13);//下标对应第几个月(静态全局数组)static int monthDayArray[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };//判断闰年if (month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)){return 29; //不要修改数组:return monthDayArray[2]++;}return monthDayArray[month];}//比较操作bool operator < (const Date& d) const;bool operator <= (const Date& d) const;bool operator > (const Date& d) const;bool operator >= (const Date& d) const;bool operator == (const Date& d) const;bool operator != (const Date& d) const;//加减天数Date& operator += (int day);Date operator + (int day) const;Date& operator -= (int day);Date operator - (int day) const;//前置(++/-- d)Date& operator ++ ();Date& operator -- ();//后置(d ++/--)Date operator ++ (int);Date operator -- (int);//日期-日期(相隔天数)int operator - (const Date& d) const;private:int _year;int _month;int _day;
};//输入输出
ostream& operator << (ostream& out, const Date& d);
istream& operator >> (istream& in, Date& d); //流提取就不能加const了
- d a t e . c p p date.cpp date.cpp 用来存放日期类方法的实现;
#include"Date.h" bool Date::CheckDate() const
{if (_month > 0 && _month < 13 && _day <= GetMonthDay(_year, _month)){return true;}return false;
}Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "非法日期:";Print();}
}Date::~Date()
{_year = _month = _day = 0;
}Date::Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}Date& Date::operator = (const Date& d)
{_year = d._year;_month = d._month;_day = d._day;return *this;
}void Date::Print() const
{cout << _year << "/" << _month << "/" << _day << endl;
}Date& Date::operator += (int day)
{if (day < 0){return *this -= (-day); //+会复用+=}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this;
}Date Date::operator + (int day) const
{Date tmp = *this;tmp += day; //复用+=:相比复用+少了3次拷贝(效率高)return tmp;
}Date Date::operator - (int day) const
{if (day < 0){return *this + (-day); //-=会复用-}Date tmp = *this;tmp._day -= day;while (tmp._day <= 0){tmp._month--;if (tmp._month == 0){tmp._year--;assert(tmp._year >= 0);tmp._month = 12;}tmp._day += GetMonthDay(tmp._year, tmp._month);}return tmp;
}Date& Date::operator -= (int day)
{*this = *this - day; //复用-:相比复用-=多了3次拷贝(效率低)return *this;
}bool Date::operator < (const Date& d) const
{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}else if (_month == d._month){return _day < d._day;}}return false;
}bool Date::operator == (const Date& d) const
{if (_year == d._year && _month == d._month && _day == d._day){return true;}return false;
}bool Date::operator <= (const Date& d) const
{return *this < d || *this == d; //复用<和==
}bool Date::operator > (const Date& d) const
{return !(*this <= d); //复用<=
}bool Date::operator >= (const Date& d) const
{return !(*this < d); //复用<
}bool Date::operator != (const Date& d) const
{return !(*this == d); //复用==
}Date& Date::operator ++ ()
{*this += 1;return *this; //前置++没有拷贝(效率高)
}Date Date::operator ++ (int)
{Date tmp = *this;*this += 1;return tmp; //后置++有2次拷贝(效率低)
}Date& Date::operator -- ()
{*this -= 1;return *this;
}Date Date::operator -- (int)
{Date tmp = *this;*this -= 1;return tmp;
}int Date::operator - (const Date& d) const
{int flag = 1; //如果假设成立Date min = d;Date max = *this;if (min > max){min = *this;max = d;flag = -1; //假设错误}int n = 0;while (min < max){++min; //自定义类型最好用前置++(不需要拷贝)n++;}return n * flag;
}ostream& operator << (ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator >> (istream& in, Date& d)
{while (1){cout << "请依次输入年月日:";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "非法日期:";d.Print();cout << "请重新输入!" << endl;cout << "-----------------" << endl;}else{return in;}}
}
- 还有一个 t e s t . c p p test.cpp test.cpp 文件用来测试我们写的日期类对不对,是否能达到我们的需求。
#include"Date.h" void test_1()
{Date d1, d2, d3, d4;d1.Print(); d2.Print(); d3.Print(); d4.Print();cout << endl;int n = 1;while (cin >> n){d1 - n;d2 + n;d3 -= n;d4 += n;cout << "d1:"; d1.Print();cout << "d2:"; d2.Print();cout << "d3:"; d3.Print();cout << "d4:"; d4.Print();}
}void test_2()
{Date d1(2025, 3, 12);Date d2 = d1;Date d3(2005,8,23);//d1.Print(); d2.Print(); d3.Print();d1 += 30000; d1.Print();d2 -= 30000; d2.Print();d3 += 30000; d3.Print();
}void test_3()
{Date d1(2025,3,12);Date d2 = d1 - 10;Date d3 = d1 + 10;Date d4 = d1;if (d1 < d2) cout << "d1 < d2" << endl;else if (d1 == d2) cout << "d1 == d2" << endl;else if (d1 > d2) cout << "d1 > d2" << endl;if (d1 < d3) cout << "d1 < d3" << endl;else if (d1 == d3) cout << "d1 == d3" << endl;else if (d1 > d3) cout << "d1 > d3" << endl;if (d1 < d4) cout << "d1 < d4" << endl;else if (d1 == d4) cout << "d1 == d4" << endl;else if (d1 > d4) cout << "d1 > d4" << endl;cout << endl;if (d1 <= d2) cout << "d1 <= d2" << endl;else cout << "d1 > d2" << endl;if (d1 <= d3) cout << "d1 <= d3" << endl;else cout << "d1 > d3" << endl;if (d1 <= d4) cout << "d1 <= d4" << endl;else cout << "d1 > d4" << endl;cout << endl;if (d1 != d2) cout << "d1 != d2" << endl;else cout << "d1 == d2" << endl;if (d1 != d3) cout << "d1 != d3" << endl;else cout << "d1 == d3" << endl;if (d1 != d4) cout << "d1 != d4" << endl;else cout << "d1 == d4" << endl;
}void test_4()
{Date d1(2025, 3, 12);Date d2 = d1;Date d3 = d1;Date d4 = d1;Date r1 = d1++; r1.Print(); d1.Print();cout << endl;Date r2 = ++d2;r2.Print(); d2.Print();cout << endl;Date r3 = d3--;r3.Print(); d3.Print();cout << endl;Date r4 = --d4;r4.Print(); d4.Print();
}void test_5()
{Date d1(2025, 3, 12), d2(2005, 8, 23);Date d3(2025, 4, 12);cout << d1 - d2 << endl;cout << d1 - d3 << endl;
}void test_6()
{Date d1, d2;cin >> d1 >> d2;//operator >> (operator >> (cin, d1), d2);cout << endl << "d1:" << d1 << "d2:" << d2 << endl;//operator << (operator << (cout, d1), d2);cout << "还有" << d2 - d1 << "天。" << endl;
}int main()
{//test_1(); //多次测试+、-、+=、-=//test_2(); //单次测试+=、-=//test_3(); //测试比较大小//test_4(); //测试++、--//test_5(); //测试d1-d2test_6(); //测试>>、<<return 0;
}
总结
以上就是对 C++ 中类和对象部分的大总结,这篇博客我前前后后码了差不多有一个周,全文差不多有 4.5 w 4.5w 4.5w 字左右,希望能对你有所帮助。