(超2万字数详解)C++学习之类与对象
经过C++入门的学习后,可能各位读者大大对于C++语法部分的内容仍然有一些困惑,但是没关系,经过后面的学习我们会对前面的内容有更好的了解。
在本篇文章中,我们会学习C++的另一个重要的部分——类与对象。
本篇博客也是作者的倾心之作品,在观看之前求一个赞,谢谢
目录
类的定义
类定义的格式
访问限定符
类域
实例化
实例化的概念
内存对齐原则
为什么要内存对齐呢?
this指针
this指针的几个小例题
C和C++ 在类之中的对比
C语言和C++在栈实现上的对比
类的默认成员函数
Date类
Stack类
Myqueue类
构造函数
析构函数
拷贝构造函数
拷贝构造的特点
赋值运算符重载
运算符重载
赋值运算符重载
流插入运算符
流提取运算符
日期类的实现
取地址运算符重载
const成员函数
取地址运算符重载
初始化列表
一道例题
类型转换
static成员
一道例题:编辑
友元
内部类
匿名对象
对象拷贝时的编译器优化
类的定义
类定义的格式
类类似于C语言中的结构体,是由其过渡而来,C++中类是数据和方法的结合。
class为定义类的关键字,Stack为类的名字,{}中为类的主题,注意类定义结束时后面分号不能省略(与C中的结构体变量)。类体中内容称之为类的成员;类中的变量称之为类的属性或成员变量;类的函数称之为类的方法或者成员函数。
为了区分成员变量,一般习惯上成员变量会加上一个特殊标识,比如成员变量前面或后面加_或者_m等。这并不是C++的规定,具体看项目组和公司的要求。
C++中struct也可以定义为类。C++兼容struct的用法,同时struct也升级为了类,很明显的变化是struct也可以定义函数。(不过我们推荐用class定义类)
定义在类中的成员函数默认为inline
下面就是一个定义类的结构示例
class stack
{//成员变量int x;int arr[100];//成员函数void init(){//code}
};
访问限定符
C++一种实现封装的方式就是用类将对象的属性和方法结合到一起,让对象更加完善,通过访问权限接口选择性的将其接口提供给外部函数使用。
public修饰的成员在类外可以直接访问;protected和private修饰的成员在类外不能直接被访问。在目前的章节我们认为protected和private,后续的学习才能体现出它们的区别。
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现位置,如果后面没有访问限定符,作用域就到},即类结束。
下图中两段代码是等效的。
class定义成员没有被访问限定符修饰的时候默认为private,struct默认为public。
举例说明:下图两段代码等效
且结果相同
一般成员函数变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
定义在类里面的函数就是内联函数。
类域
在C++入门C++学习记录:C++入门-CSDN博客的命名空间片段中我们知道类域属于命名空间的一部分,接下来我们将就类域进行详细地叙述。
目前我们知道的有四个域:局部域全局域、类域和命名空间域。命名空间域和类域不影响声明周期,只解决各自命名冲突(类域解决互相之间的命名冲突,命名空间域解决全局的命名冲突)
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员的时候,需要使用::作用域操作符指明成员属于哪个类域
如果声明与定义分离,可以按如下方式去指定的类域寻找init函数,在预处理处包含<stack.h>(举例说明的)
如果声明与定义分离,可以按如下方式去指定的类域寻找init函数,在预处理处包含<stack.h>(举例说明的)
void stack::init();//去指定的类域中寻找init函数
类域解决了类与类之间的命名冲突。命名空间域是解决全局空间的函数/变量/类型的命名冲突。
类域影响的是编译的查找规则。比如在下图的程序中init如果不指定类域stack,那么编译器就会把init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域stack,就是知道init是成员函数,如果当前域找不到,就回去类域当中寻找
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class stack
{
public://成员函数void init(int n = 4);
private://成员变量int* array;size_t capacity;size_t top;
};
void stack::init(int n)
{array = (int*)malloc(sizeof(int) * n);if(nullptr == array){perror("malloc申请空间失败");return;}capacity = n;top = 0;
}int main()
{stack st;st.init();return 0;
}
实例化
实例化的概念
类比于现实生活中的案例,我们知道房子在建造之前需要先画图纸,房子是依照于图纸建造的,但是图纸并不能住人,只有把它实体化作为房子才能住人。实例化同样如此,类中只能声明数据,不能存储数据,只有把它实例化了才能分配物理内存储存数据。
因此我们说:
用类类型在物理内存中创建对象的过程,称之为类实例化对象。
类是对象进行一种抽象的描述,是一个模型一样的东西,限定了类中有哪些成员变量,这些成员变量只有声明没有分配空间,用类实例化出对象时,才会分配空间。
一个类可以实例化出多个对象,实例化出来的对象,占用实际的物理空间,储存类成员变量。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Date
{
public:void init(int year, int month, int day){_year = year;_month = month;_day = day;}
public://为了区分成员变量//一般习惯上在成员变量前面加标识符//如:_,m等int _year;//这是声明不是定义int _month;int _day;
};
int main()
{Date d1;//给变量开辟空间//类实例化对象d1.init(2025, 4, 23);d1._year++;cout << sizeof(Date) << endl;cout<<sizeof(d1)<<endl;return 0;
}
在C++中同C语言一样,实例化的对象依然遵循内存对其原则。
结果如下
这是为什么呢?接下来我们来解析一下:
首先我们需要了解内存对齐原则(具体如下)
由第三条和第四条可知,_year、_month、_day对齐数均为4.
但是实际结果却为12,这是为什么呢?因为实际上而言,类实例化的对象只包含成员函数的变量,不包含成员函数的指针。
进一步来说,成员变量每次定义的时候需要各自向内存申请一份空间,是独立的空间;但是成员函数不同,成员函数每次调用的时候无论多少个对象调用的均为同一个函数,因此成员函数的指针的存储就重复了,这是没有必要的。
以下图的代码为例,解释在注释之中。
内存对齐原则
1.第一个成员在于结构体前一辆为0的地址处。
2.其他成员变量要对齐到某个数字(对其书)的整数倍的地址处
3.注意:对齐数=编译器默认的一个对其书于该成员大小的比较值
4.VS中默认的对齐值为8
5.结构体总大小为:最大对其数(所有变量类型最大者于默认对齐参数取最小)的整数倍
6.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的最大对其书)的整数倍
接下来我们要举例说明:比较下列类A、B、C实例化后的对象有多大。代码如下:
//计算A/B/C三个类的实例化对象大小
class A {
public:void printf(){cout << "_ch" << endl;}
private:char _ch;int _i;
};
class B {
public:void print(){//}
};
class C
{};
int main()
{A a;B b;C c;cout << sizeof(A) << endl;cout << sizeof(B) << endl;cout << sizeof(C) << endl;return 0;
}
预期结果:8 0 0
实际结果:
为什么是这样的呢?我们来详细地解释一下。
8的解释如下:
那么为什么明明class B和class C没有成员变量,还有一个1呢?因为如果一个字节不给,怎么表示对象存在过?所以这里给1个字节,只是为了占位标识对象而存在的。
为什么要内存对齐呢?
在内存的读取中,我们并不是从任意位置开始读的(这里涉及一部分硬件相关的数据总线的知识),而是从整数倍开始读的。由上图展示,当我们内存对齐的时候想要访问_i的时候,只需要从第二个绿线开始即可。但是如果我们不对齐地访问_i的话,只能从第一个紫色位置访问3个字节再从第二个紫色位置后的一个位置访问一个字节拼接而成(第二个绿线和第二个紫线指向的是同一个位置)。实际上,内存对齐是一个用空间换取时间,牺牲一部分内存换取效率提升的方式
如下图所示,都是从整数倍开始读。
this指针
Date类中有init与print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用init和print函数的时候,该函数是如何知道应该访问的是d1对象还是d2对象呢?这就要看C++给了一个隐含的this指针解决问题。
this指针是一个关键字。
this指针会将函数的定义处理成这个样子
编译器编译后,类的成员函数默认都会在形参的第一个位置增加一个当前类类型的指针,叫做this指针。比如Date类的init原型为
void init (Date * const this,int year,int month,int day)
类的成员函数中访问成员变量,本质都是通过this指针访问的,如init函数中给_year赋值
this->_year=year
C++规定不能在实参和形参的位置显示的位置写this指针(编译器再编译的时候会自行处理),但是可以在函数体内显示使用this指针
this指针的几个小例题
1、下列代码运行的结果(C)
A、编译报错B、运行崩溃C、正常运行
#include<iostream>
using namespace std;
class A
{
public:void print(){cout<<"A::print()"<<endl;}
private:int _a;
};
int main()
{A*p=nullptr;p->print();return 0;
}
解析:在main函数中,A类的指针p为空。但是并没有对p进行解引用,print()编译成指令后就是call的print的地址。那么p指针的用途是什么呢?由上面我们知道成员函数print函数不存放在类中。在编译的时候我们还需要找到它的出处:A类型的指针。第二:对象或者对象的指针话要传递this指针,就要将p的地址传递给print函数。虽然p是空指针,但是并没有对p进行解引用,所以程序正常运行
编译错误是语法问题
空指针的解引用是运行问题
2、下列代码运行B)
A、编译报错B、运行崩溃C、正常运行
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class A
{
public:void Print(){cout<<"A::Print()"<<endl;cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print(); return 0;
}
解析:这段代码与上面类似,唯一的区别是它打印了-a,这就导致了问题的出现。核心的问题就是-a被解引用了。this指针访问-a,对-a进行了解引用,导致了问题的出现。
3.this指针存在于内存的哪个A)
A.栈 B.堆 C.静态区 D.常量区 E.对象里面
解析:this指针是一个形参。形参的本质是函数调用才在栈区开辟空间。
常量区的数据是代码段
C和C++ 在类之中的对比
面向对象的三大特性:封装、继承、多态。(只是最著名的不是只有三个)
C++实现类形态上发生了很多变化,底层和逻辑没什么变化。
C++中数据和函数都放到了类中,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是封装的一种表现,也是最重要的变化。这里的封装本质上是一种更严格归发的管理,避免出现乱访问修改的问题。当然封装不仅仅如此,我们后面还要不断地去学习完善。
C++中有一些相对方便的语法,比如init给的缺省参数很方便,成员函数每次不需要传对象地址。因为this指针隐含地传递了,方便了很多。使用类型不再需要typedef,使用类名就很方便。
C++入门阶段实现的stack看起来变化了很多,实质上变化不大。后续学习STL中适配器实现的stack会更能感受到iC++的魅力
下面我们通过C和C++ 分别实现stack的两段代码来解析
C语言代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void Stinit(ST* ps)
{assert(ps);ps->a = NULL;ps->top = 0;ps->capacity = 0;
}
void STdestory(ST* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = 0;ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{assert(ps);if (ps->top == ps->capacity){int newcapacity=ps->capacity==0?4:ps->capacity*2;STDataType* tmp=(STDataType*)malloc(ps->a,newcapacity*sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}ps->a[ps->top] = x;ps->top++;}
}
bool STEmpty(ST* ps)
{assert(ps);return ps->top == 0;
}void STPop(ST* ps){assert(ps);asser(!STEmpty(ps));ps->top--;}
STDataType STTop(ST* ps)
{assert(ps);assert(!STEmpty(ps));return ps->a[ps->top - 1];
}
int STSize(ST*ps)
{assert(ps);return ps->top;
}
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);}STdestory(&s);return 0;
}
C++代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
class Stack
{
public://成员函数void init(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType)*n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}void push(STDataType x){if (_top == _capacity){int new_capacity = _capacity * 2;STDataType* new_a = (STDataType*)malloc(sizeof(STDataType) * new_capacity);if (tmp == NULL){perror("malloc申请空间失败");return;}_a = tmp;_capacity = new_capacity;}_a[_top] = x;}void Pop(){assert(_top>0)--_top;}bool Empty(){return _top==0}int Top(){asset(_top > 0);return _a[_top - 1];}void Destory(){free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
int main()
{Stack s;s.init();s.push(1);s.push(2);s.push(3);s.push(4);cout << s.Top() << endl;while (!s.Empty()){printf("%d \n",s.Top());}s.Destory();return 0;
}
C语言和C++在栈实现上的对比
C语言与C++在栈(Stack)数据结构实现上的关键差异对比表格:
特性 | C语言实现 | C++实现 | 关键差异 |
---|---|---|---|
实现方式 | 使用结构体+函数手动管理(面向过程) | 使用类(Class)封装数据和方法(面向对象) | C++通过类实现封装和数据隐藏,C通过结构体和全局函数暴露细节。 |
内存管理 | 手动分配/释放内存(malloc /free ),需防范内存泄漏。 | 支持RAII(构造/析构函数自动管理资源),可结合智能指针(如std::unique_ptr )。 | C++自动资源管理更安全,C需手动控制易出错。 |
错误处理 | 通过返回值或全局变量传递错误(如返回-1 表示栈满)。 | 支持异常处理(try /catch ),可抛出栈溢出等异常。 | C++错误处理更结构化,但异常可能影响性能;C依赖约定俗成的错误码。 |
类型安全 | 通常基于void* 实现通用栈,需强制类型转换,类型不安全。 | 可通过模板(template )实现类型安全的泛型栈。 | C++编译时类型检查避免错误,C需开发者保证类型正确性。 |
默认操作 | 需显式调用初始化函数(如init_stack() )和销毁函数(destroy_stack() )。 | 构造函数(初始化)和析构函数(清理资源)自动调用。 | C++减少冗余代码,C需手动管理生命周期。 |
扩展性 | 功能固定,扩展需修改结构体或添加新函数。 | 可通过继承、组合或模板特化扩展功能(如线程安全栈)。 | C++面向对象特性支持灵活扩展,C需重新实现或复制代码。 |
语法特性 | 仅支持基础语法(函数指针可选)。 | 支持运算符重载(如重载<< 压栈)、友元函数等。 | C++语法更丰富,可自定义栈操作符行为,C仅能通过函数调用。 |
标准库支持 | 无内置栈库,需手动实现。 | 提供std::stack 模板类(容器适配器)。 | C++可直接使用标准库栈,C需从零实现。 |
类的默认成员函数
默认成员函数没有显式实现,编译器会自动生成成员函数称之为默认成员函数。一个类我们在不写编译i去会默认生成6个以下的默认成员函数,需要注意的是这6个最重要的是前4个,最后两个不太重要,了解即可。
1.构造函数:对象实例化时初始化成员变量
2.析构函数:对象销毁时候清理资源
3.拷贝构造函数:用已存在对象初始化新对象
4.赋值运算符重载:对象间赋值操作
5.普通取地址重载:获取普通对象的地址(类类型为*operator&())
6.const取地址重载:获取const对象的地址(const类类型*operator&()const)
其次是C++11以后会增加两个默认成员函数,移动构造和移动赋值,我们可以后面学习。成员函数也很重要,也比较复杂。我们主要学习两个方面。
第一:我们不写时,编译器会默认生成函数的行为是什么?是否满足我们的需求
第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么我们要如何自我实现呢?
Date类
class Date
{
public:1无参数构造函数//Date()//{// _year = 1;// _month = 1;// _day = 1;//}2有参数构造函数//Date(int year,int month,int day)//{// _year = year;// _month = month;// _day = day;//}//3全缺省的构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}void print(){cout << _year << "/" << _month << "/" << _day << endl;}bool operator==(const Date& e)const//也可以这样写operator =(Date d1, Date d2){return _year == e._year&& _month == e._month&& _day == e._day;}
private:int _year;int _month;int _day;
};
Stack类
//Stack类
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st){//需要对_a指向的资源进行深拷贝_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, st._a, sizeof(STDataType) * st._capacity);_top = st._top;_capacity = st._capacity;}void push(STDataType x){if (_top == _capacity)//满了扩容{int new_capacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * new_capacity);if (tmp == nullptr){perror("realloc申请空间失败");return;}_a = tmp;_capacity = new_capacity;}_a[_top++] = x;}int top(){return _a[_top - 1];}~Stack(){cout << "析构函数~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}//private:STDataType* _a;int _capacity;int _top;
};
Myqueue类
class Myqueue
{
public:
private:Stack pushst;Stack popst;
};
构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名字叫构造,但是构造函数主题任务并不是开空间创建对象(我们常用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的init 函数的功能,构造函数自动调用的特点就完美替代了init。
构造函数的特点:
1.函数名与类名相同。
2.无返回值(返回值啥也不需要给,也不需要写void,这是C++规定的,不用纠结为什么)
3.对象实例化时系统会自动调用对应的构造函数
4.构造函数可以重载
如下图所示的代码,_year、_month、_day、均被初始化为1.
它们的传递关系是这样的 。
观察我们发现d2传递的是有参数的写成
Date d2(2025, 4, 26);
但是我们d1不能写成这样
Date d1()
为什么呢?为了易于区分于函数声明
//函数声明
Date func();
//对象的定义
Date d3;
因此我们知道:对象实例化一定会调用对应的构造,保证了对象实例化出来一定被初始化了。
科普:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据模型,如:int/char/double/指针等,自定义类型就是我们使用的class/struct等关键字自定义的类型。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数。一旦用户显式定义编译器将不再生成。
下图的代码为什么这里会这样呢?
因为不写构造,编译生成默认构造,但是这里我们已经写了构造,所以还会报错
6.无参构造函数、全省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且仅有一个存在,不能同时存在。无参构造函数和全省构造函数虽然构成函数重载,但是调用时候会产生歧义(参考上篇博客函数重载的范围)。可能有些人会认为默认构造函数是编译器默认生成的那个叫默认构造,实际上无参构造函数、全省构造函数也是默认构造函数。总结一下就是不传实参就可以调用的构造就叫默认构造。
构造函数在大多数场景下需要自己去实现,只有少数myqueue这样才编译器生成就OK。
7.我们不写、编译器默认生成的构造函数堆内置成员变量初始化没有任何要求,也就是说是是否初始化是不确定的,看编译器,对自定义类型成员变量要求调用这个成员变量的默认狗早函数初始化。如果这个成员变量没有默认构造函数,那就会报错。我们要初始化这个成员变量,需要用初始化列表(后续学习)才能解决。
析构函数
析构函数与构造函数功能相反, 析构函数不是完成对本想本身的销毁,比如局部对象存在栈帧的函数结束栈帧销毁他就释放了。不需要我们管,C++规定在销毁的时候会自动调用析构函数,完成对象中资源的清理和释放的工作。析构函数的功能类比于我们之前Stack实现的destory功能,而像Date没有destory,其实就是没有资源需要释放。所以严格来说Date是不需要析构函数的。
下图就是析构函数的一个小小的应用。
析构函数的特点:
1.析构函数名就是类名前加上~
2.无参数无返回值(与构造类似,也不需要void)
3.一个类只能有析构函数。若未定义,系统会生成默认的析构函数
4.对象生命周期结束时,系统会自动调用析构函数。
5.跟构造函数类似,我们不写编译器自动生成的析构函数堆内置类型成员不做处理。自定类型成员会调用它的析构函数。
6.还需要注意的是我们显示写析构函数,对自定义类型的成员也会调用它的析构函数,也就是说自定义类型成员无论生命情况下都会自动调用析构函数。
7.如果类中没有申请资源的时候,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构函数可用就不需要显示写析构,如MyQueue;但是没有资源申请的时候,一定要自己写析构函数,否则会造成资源泄漏,如Stack。
8.一个局部域内多个对象,C++规定后定义的先析构。
默认生成的析构函数对内置类型军不做处理,对于自定义类型会去调用对应的构造函数和析构函数。
大多数的类不需要显式写构造,少数的需要写;大多数的类需要显式写析构,少数的不需要写
typedef int STDataType;
class Stack
{
public:Stack(int n=4){_a=(STDataType*)malloc(sizeof(STDataType)*n);if (nullptr == _a){perror("malloc申请空间失败");return ;}_capacity=n;_top=0;}~Stack(){cout << "析构函数~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;int _capacity;int _top;
};
拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认的值,则此构造函数,也就是说拷贝函数是一个特殊的构造函数。
class Date
{
public:1无参数构造函数//Date()//{// _year = 1;// _month = 1;// _day = 1;//}2有参数构造函数//Date(int year,int month,int day)//{// _year = year;// _month = month;// _day = day;//}//3全缺省的构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year=d._year;_month=d._month;_day=d._day;}Date(Date *d){_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 d)
{cout << "&d" << endl;d.print();
}
Date& func2(Date* d)
{Date tmp(2024, 5, 1);tmp.print();return tmp;
}
int main()
{Date d1(2025, 5,1);//C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造//所以d1传值传参给d要调用拷贝构造,传引用用在这里比较少见func1(d1);cout<< &d1 << endl;//这里完成拷贝,但是不是拷贝构造,只是普通的构造Date d2(&d1);d1.print();d2.print();//这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针构造Date d3(d1);d2.print();//也可以这样写Date d4 = d1;d2.print();//Func2返回了一个局部tmp对象作为返回值,返回值是引用,所以可以直接修改tmp的值//Func2函数结束,tmp对象销毁,相当于一个野引用return 0;
}
拷贝构造的特点
1.拷贝构造函数是构造函数的重载
Data d1(2025,4,28);//构造函数
Date d2(d1);//拷贝构造函数
2.拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值传参编译器直接报错,因为语法逻辑上会引发无穷递归。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
在C++中,自定义类型的对象,在直接拷贝和传值传参中都要调用拷贝构造函数。
不能使用传值传参会形成无穷递归的原因如下图所示:每次调用拷贝构造之前都要传值传参,传值传参本质上是一种拷贝,又形成了一个拷贝构造,如此递归下去形成了无穷递归。
引用传参
指针也可以,但是写法上很奇怪,
Date d3(&d1);//只是普通的构造
3.C++规定子当以类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
4.若未显示定义拷贝构造,编译器会生成自动生成拷贝函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
如上图所示的代码中, 程序会崩溃。为什么呢?
如上图所示,st2相较于st1是后构造的函数,也就是st2先析构,所指向的空间内存销毁了,但是st1析构的时候空间,因此就会导致程序析构两次,也就是浅拷贝问题。栈帧浅拷贝存在问题,因为它们的数据不存在在对象上,而存放在它们所指向的空间上。 所以会导致两个问题:
(1)一个修改会影响另一个。
(2)析构两次资源。
因此就导致了程序的崩溃。
深拷贝:
//深拷贝Stack(Stack& s){_a=(STDataType*)malloc(sizeof(STDataType)*s._capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, s._a, sizeof(STDataType)*s._capacity);_top = s._top;_capacity = s._capacity;}
对于Date类来说,浅拷贝和深拷贝没什么区别。但是对于栈来说浅拷贝会导致如上的问题,只能进行深度拷贝
析构和拷贝几乎同时出现的。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
class Stack
{
public://成员函数void init(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}
//构造函数Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}//深拷贝Stack(const Stack& s){_a=(STDataType*)malloc(sizeof(STDataType)*s._capacity);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, s._a, sizeof(STDataType)*s._capacity);_top = s._top;_capacity = s._capacity;}void push(STDataType x){if (_top == _capacity){int new_capacity = _capacity * 2;STDataType* new_a = (STDataType*)malloc(sizeof(STDataType) * new_capacity);if ( new_a == NULL){perror("malloc申请空间失败");return;}_a = new_a;_capacity = new_capacity;}_a[_top++] = x;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
class myqueue
{private://默认生成的类型生成值拷贝Stack _pushst;Stack _popst;public:
};
int main()
{Stack s1;myqueue q1;myqueue q2(q1);//构造不需要自己写,默认生成的构造会去调用它的构造//析构不需要自己写,默认生成的析构会去调用栈的析构//拷贝构造需要自己写,默认生成的拷贝构造会去调用栈的拷贝构造return 0;
}
5.像Date类这样的类成员变量全是内置类型且没有指向什么资源,比那一其自动生成一个拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向资源也进行拷贝)。像myqueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造,也不需要我们显式实现myqueue的拷贝构造。这里还有一个小技巧,如果一个类显式实现了析构并释放资源,那么就不需要显式写拷贝构造,否则就需要。
typedef int STDataType;
class Stack
{
public:Stack(int n=4){_a=(STDataType*)malloc(sizeof(STDataType)*n);if (nullptr == _a){perror("malloc申请空间失败");return ;}_capacity=n;_top=0;}Stack(const Stack& st){//需要对_a指向的资源进行深拷贝_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, st._a, sizeof(STDataType) * st._capacity);_top = st._top;_capacity = st._capacity;}void push(STDataType x){if (_top == _capacity)//满了扩容{int new_capacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * new_capacity);if (tmp == nullptr){perror("realloc申请空间失败");return;}_a = tmp;_capacity = new_capacity;}_a[_top++] = x;}int top(){return _a[_top - 1];}~Stack(){cout << "析构函数~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;int _capacity;int _top;
};
int main()
{Stack st1;st1.push(1);st1.push(2);//Stack不显示拷贝构造,用自动生成的拷贝构造就完成浅拷贝//会导致st1和st2的_a指针指向同一片资源,析构两次,程序崩溃Stack st2 = st1;Myqueue mq1;//Myqueue自动生成的拷贝构造会自动调用Stack的拷贝构造完成pustst和popst的深拷贝//只要Stack拷贝构造自己实现了神拷贝,它就没有问题return 0;
}
6.传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝,但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用(类似于野指针)。传引用返回可以减少拷贝,但是一定要确保返回对象在当前函数结束后还在才能用引用返回
如下面代码所示,为传值返回,返回的是ret的拷贝
int f1(int a=1)
{int a = 10;return a;
}
而传引用返回,返回的是它的别名。但是ret出了作用域ret就随着栈帧销毁了,所以返回ret是野引用。
而如下所示的代码第二行的cout之所以这么写是因为f2函数本质上是个栈,返回的是一个栈对象,因此这么写可以访问栈上的数据。
Stack &f2()
{Stack st;st.push(1);st.push(2);st.push(3);return st;
}
int main()
{cout << f1() << endl;cout<<f2().top()<<endl;return 0;
}
而上图所示的代码运行后程序崩溃。为什么呢? 原理图如下:
st出了f2函数后就会st会去调用析构函数(完整代码如下)释放掉空间,但是st虽然销毁了但是仍然能访问到(引用的底层是指针),类似于野引用。访问top函数和_a(相当于空指针),返回的是st的别名。自定义类型深拷贝容易无问题。
如果先用传引用返回可以这么写
Stack &f2()
{static Stack st;st.push(1);st.push(2);st.push(3);return st;
}
int main()
{cout << f1() << endl;cout<<f2().top()<<endl;return 0;
}
完整代码:
typedef int STDataType;
class Stack
{
public:Stack(int n=4){_a=(STDataType*)malloc(sizeof(STDataType)*n);if (nullptr == _a){perror("malloc申请空间失败");return ;}_capacity=n;_top=0;}Stack(const Stack& st){//需要对_a指向的资源进行深拷贝_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}memcpy(_a, st._a, sizeof(STDataType) * st._capacity);_top = st._top;_capacity = st._capacity;}void push(STDataType x){if (_top == _capacity)//满了扩容{int new_capacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * new_capacity);if (tmp == nullptr){perror("realloc申请空间失败");return;}_a = tmp;_capacity = new_capacity;}_a[_top++] = x;}int top(){return _a[_top - 1];}~Stack(){cout << "析构函数~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;int _capacity;int _top;
};
int& f1()
{int ret = 10;return ret;
}
Stack &f2()
{Stack st;st.push(1);st.push(2);st.push(3);return st;
}
int main()
{cout << f1() << endl;cout<<f2().top()<<endl;return 0;
}
赋值运算符重载
运算符重载
1.当运算符被用于类类型的对象的时候,C++允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符的时候,必须转换为调用对应运算符重载。若没有对应运算符重载,则编译报错
如下的代码就会报错。
2.运算符重载是具有特殊名字的函数,名字是由operator和后面定义的运算符共同筑成的。和其他函数一样,它也具有返回类型、参数列表、函数体
我们举例说明:
bool operator==(const Date &e1,const Date &e2)
//也可以这样写operator =(Date d1, Date d2)
{return e1._year == e2._year&&e1._month == e2._month&&e1._day == e2._day;
}
结果为:
但是编译不通过(因为类外不能访问私有)
传引用传参不改变值的时候加上const
解决方法:
(1)将类中私有改为公有(不推荐)。
(2)提供GetXXX函数
(3)友元(后面详细说明)
(4)将其放入类之内
3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符一个参数,二元运算符两个参数,三元运算符三个参数。二元运算符的左侧运算对象传给第一个参数,右侧运算对象传递给第二个参数
4.如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传递给隐式的this指针,因此运算符重载作为成员函数时候参数比运算对象少一个。
但是如果直接放入类之内就会报错,具体原因就是特点4
所以我们可以这样改
bool operator==(const Date &e)
//也可以这样写operator =(Date d1, Date d2)
{return _year == e._year&&_month == e._month&&_day == e._day;
}
5.运算符重载后,其优先级和结合应与内置类型运算符保持一致。
6.不能通过链接语法中没有的符号来创建新的操作符好,比如operator@
7.
.* :: sizeof ?: .
以上五个运算符不能重载。 (需要记忆)
.* 运算符是C++类型专属的运算符(了解即可,应用较少)
class A
{
public:void func(){cout<<"A::func()"<<endl;}
};
void f()
{cout << "f()" << endl;
}
int main()
{//函数指针void(*func1)() = f;//函数指针的调用(*func1)();//成员函数的指针//默认只能全局搜索和局部搜索,所以要指明类域void(A::*func2)() = &A::func;A aa;(aa.*func2)();//对成员函数解引用,调用成员函数指针return 0;
}
8.重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如
int+operator+(int x,int y)
9.一个类需要哪些运算符是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+没有意义
10.重载++运算符时候有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载的时候,增加一个int形参,跟前置++构成函数重载,方便区分。
11.重载<<和>>时候,需要重载为全局变量,因为重载为成员函数,this指针默认抢占第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局变量把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
赋值运算符重载
赋值运算符重载是一个默认的成员函数,用于完成两个已经存在的对象直接的拷贝赋值,要注意拷贝构造区分,拷贝构造用于一个对象初始化给另一个要创建的对象。
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const当前类型引用,否则会传值传参会有拷贝。
2.有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景
3.没有显式实现时,编译器会自动生成一个莫i人赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员会完成值拷贝/深拷贝(一个字节一个字节拷贝),对自定义类型成员变量会调用它的赋值重载函数。
4.像Date类这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己去实现深拷贝(对指向资源也进行拷贝)。像Myqueue这样的类型内部主要是自定义类型的Stack成员。编译器自动生成 的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显式实现Myqueue的赋值运算符重载。
这里还有一个小技巧:如果一个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载,否则就不需要。
流插入运算符
流插入运算符 <<。
cout<<能实现无限制输出是因为其本质上就是函数重载,cout在库内就已经进行了重载。
int i=1;
double d=2.3;
cout<<i;
cout<<d;
cout<<operator(i);
cout<<operator(d);
而这么写本质上是三次的函数调用。流插入运算符结合性是从左向右
int i=1;
double d=2.3;
cout<<i<<d<<endl;
流插入的一种重载方式
ostream& operator<<(ostream& out)
{out<<_year<<"-"<<_month<<"-"<<_day;
}
只要一个类重载了流插入和流提取任何类型都可以用(包括自定义类型)
流提取运算符
其余部分与流插入类似。
istream& operator<<(istream& in, Date& d)
{cout<<"请输入日期(格式:yyyy-mm-dd):>";in>>d._year>>d._month>>d._day;
}
日期类的实现
Date.cpp——函数实现
Date.h——类声明与函数声明
test.cpp——方法实现
Date.h
#pragma once
#include<assert.h>
#include<iostream>
using namespace std;
class Date
{//友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator<<(istream& in, Date& d);
public://日期加减Date(int year = 1, int month = 1, int day = 1);void print()const;//频繁调用//直接定义在类之中,默认是inlineint GetMonthDay(int year, int month){assert(month > 0 && month <= 12);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;}else{return monthdayarray[month];}}bool CheckDate();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;//d1+=天数Date&operator+=(int day);Date operator+(int day)const;//d1-=天数Date& operator-=(int day);Date operator-(int day)const;//d1-d2int operator-(const Date& d)const;//让两者强行重载// 为了区分,构成重载,给后置++强行加上一个int形参// 不需要形参的名字,因为接受值是多少不重要也不需要// 仅仅是为了与前置++区分//前置++Date &operator++();//后置++Date operator++(int);//前置--Date &operator--();//后置--Date operator--(int);
private:int _year;int _month;int _day;
};
//连续的流提取
ostream& operator<<(ostream& out, const Date& d);
istream& operator<<(istream& in, Date& d);
Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
bool Date::CheckDate()
{if (_month < 1 || _month>12||_day < 1 || _day>GetMonthDay(_year, _month)){return false;}else{return true;}
}
Date ::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "非法日期!!" << endl;}
}
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 > 12){_year++;_month = 1;}}return *this;
}
Date Date::operator+(int day) const
{Date tmp=(*this);tmp += day;return tmp;
}
//日期-天数
Date&Date:: operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day<=0){--_month;if (_month == 0){_year--;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;
}Date Date:: operator-(int day)const
{Date tmp=(*this);tmp -= _day;return tmp;
}
//前置++
Date& Date::operator++()
{*this += 1;return *this;
}
//后置++
Date Date::operator++(int)
{Date tmp(*this);*this += 1; return tmp;
}
//前置--
Date& Date::operator--()
{*this -= 1;return *this;
}
//后置--
Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}
bool Date::operator==(const Date& d)const
{return _year == d._year && _month == d._month && _day == d._day;
}
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
{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}}else if (_year == d._year&& _month == d._month){if (_day < d._day){return true;}}return false;
}
// d1 <=d2
//*this<=d
bool Date::operator>=(const Date& d)const
{return !(*this < d);
}
bool Date::operator<=(const Date& d)const
{return *this<d || *this == d;
}
int Date::operator-(const Date& d)const
{Date max = (*this);Date min = d;int flag = 1;if (*this < d){max = d;min = (*this);flag = -1;}int n = 0;while (min!=max){++min;++n;}return flag*n;
}
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day<< "日" << endl;
}
istream& operator<<(istream& in, Date& d)
{cout << "请输入日期:>";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "非法日期!!" << endl;}
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
void TestDate1()
{Date d1(2025,5,3);Date d2=d1+100;d1.print();d2.print();Date d3(2025, 5, 3);Date d4 = d3 - 5000;d3.print();d4.print();Date d5(2025, 5, 3);d5 += 5000;d5.print();
}
void TestDate2()
{Date d1(2025, 5, 1);Date d2 = ++d1;d1.print();d2.print();Date d3 = d1++;d1.print();d3.print();
}
void TestDate3()
{Date d1(2025, 5, 2);Date d2(2035, 5, 3);int n=d2-d1;cout<<"n="<<n<<endl;
}
void TestDate4()
{Date d1(2025, 5, 1);Date d2=d1+3000;cout << d1;cout << d2;
}
void TestDate5()
{const Date d1(2025, 5, 1);d1.print();d1 + 100;Date d2(2025, 4, 25);d2 += 100;d2.print();}
int main()
{//TestDate1();//TestDate2();//TestDate3();//TestDate4();//TestDate5();return 0;
}
取地址运算符重载
const成员函数
讲const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表后面。
const实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行i需改。const修饰Date类的print成员函数,print隐含的this指针由
Date*const this
变为
const Date*const this
比如下面这段代码之所以会报错是因为涉及到了权限的放大。
const修饰的成员函数是这样子的。
void Date::print()const
{cout << _year << "-" << _month << "-" << _day << endl;
}
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成就我们就可以用了,不需要显示实现。除非很特殊的场景,如我们不想让别人取当前类对象的地址,就可以自己一份,随便返回一个地址。
取地址会默认取出最匹配自己的地址(如果没有const函数d2也会匹配。)
“Date.h”
Date* operator&()
{return this;
}
const Date* operator&() const
{return this;
}
"test.h"
void TestDate3()
{Date d1(2025, 5, 3);const Date d2(2025, 5, 3); cout<<&d1<<endl;cout<<&d2<<endl;
}
初始化列表
1.之前我们实现构造函数的时候,初始化成员主要使用函数体内赋值,构造函数初始化还有一种方式:初始化列表。初始化列表使用方式是以一个冒号开始的,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式
class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day)//这就是初始化列表初始化{//code}
private:int _year;int _month;int _day;
};
2.每个成员变量都在初始化列表中只能出现一次。语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
3.引用成员变量,const成员变量,没有默认构造的类类型对象,必须放在初始化列表位置进行初始化,否则编译报错。
不可以引用形参。如果要改可以这么改变
而下图所示的Time t是没有默认构造的(默认构造就是不传参就可以调用的,还有两种:无参数和全缺省参数)
4.尽量使用初始化列表初始化,因为哪些你不在初始化列表初始化的成员也会走初始化列表。如果该成员在声明位置给了初始值则初始化列表用缺省值初始化;如果没有缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并未规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造函数就会编译错误。
下图所示的不是初始化,是声明添加缺省值
初始化列表
5.初始化列表中按照成员变量在类中声明顺序初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化顺序列表保持一致
总结:
无论是否显示写初始化列表,每个构造函数都有初始化列表
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化
下图则是初始化列表相关的思维导图。
Stack类初始化列表改造
typedef int STDataType;
class Stack
{
public:Stack(int n = 4) : _a((STDataType*)malloc(sizeof(STDataType)* n)), _top(0), _capacity(n){if (nullptr == _a){perror("malloc申请空间失败");return;}//其他构造函数代码}private:STDataType* _a;int _top;int _capacity;
};
对于stack类来说,虽然能通过这是很奇怪的写法
一道例题
1.
#include<iostream>
using namespace std;
class A
{
public:A(int _a):_a1(_a), _a2(_a1){}void print(){cout << _a1 << "_" << _a1 << endl;}private:int _a1 = 1;int _a2 = 2;
};
int main()
{A aa(1);aa.print();return 0;
}
程序输出结果: 输出1随机值
在main函数中我们首先将_a的值1传递给类,根据初始化列表可知我们先初始化_a2,但是_a2的数值_a1未初始化,所以_a2为随机值,_a1为1。因此结果就是如上
类型转换
C语言中也有类型转换,但是主要集中在整型之间可以转换,整型与浮点型、整型与指针、指针与指针。内置类型和自定义类型是不能转换的。
而在C++中,C++支持内置类型隐式转换为类类型对象,需要有相关内置类型为参数的构造函数。
构造函数前面+explicit就不再支持隐式类型转换。
类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class A
{/*构造explicit就不再支持隐式转换了explicit A(int a1)*/
public:A(int a1):_a1(a1){}//explicitA(int a1,int a2)A(int a1, int a2):_a1(a1), _a2(a2){}void print(){cout<<_a1<<" " << _a2 << endl;}int Get()const{return _a1 + _a2;}
private:int _a1=1;int _a2=2;
};
class B
{
public:B(const A& a):_b(a.Get()){}
private:int _b = 0;
};
int main()
{//1构造一个A的临时对象,再这个临时对象拷贝构造一个aa//编译器遇到连续构造+拷贝构造->优化为直接构造A aa1=1;aa1.print();const A& aa2 = 1;//C++11后支持多参数转化A aa3(2,2);//aa3隐式类型转换为b对象//原理类似const B& bb = aa3;return 0;
}
static成员
1.用static修饰的成员变量称之为静态成员变量,静态成员变量一定要在类外进行初始化
2.静态成员变量为所有类对象共享,不属于任何具体的对象、不存在对象中,存放在静态区
3.用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
4.静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针;非静态的成员函数可以访问任意的静态成员变量和静态成员函数
5.突破类域就可以访问静态成员,可以通过“类名::静态成员”或者“对象.静态成员”来访问静态成员变量和静态成员函数。
6.静态成员也是类的成员,受public、protected、private访问限定符限制
7.静态成员变量不能在生命位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++_scount;}~A(){--_scount;}//没有this指针static int GetCount(){return _scount;}int Get(){return _scount;}
private://不属于某个对象。属于整个类,这个类所有对象共有//类内声明变量static int _scount;
};
//类外初始化
int A::_scount = 0;int main()
{A a11;/*cout << A::_scount << endl;*/cout<<a11.Get()<<endl;return 0;
}
一道例题:
答案:(该代码涉及变长数组,在VS下无法编译)
class sum
{
public:sum() {_count += _i;++_i;} // 正确的构造函数定义格式static int GetCount(){return _count;}
private:static int _i;static int _count;
};
class soultion
{
public:int sum_soultion(int n) {//变长数组sum arr[n];return ::sum::GetCount();}
};
友元
在类外不能访问私有成员的。但是C++提供了一个叫友元的东西可以帮助我们实现。
1.友元提供了一种突破类访问限定符封装的方式。友元分为:友元函数和友元类。在函数声明或者类声明的前面加friend并且把友元声明放到一个类里面。
2.外部友元函数可访问的私有和保护成员,友元函数仅仅是个声明,他不是类的成员函数。
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4.一个函数可以是多个类的友元函数
5.友元类的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员
6.友元类的关系是单向的,不具有交换性。比如A类是B类的友元函数,但是B类不是A类的友元函数。友元函数的关系也不能传递,如果A是B的友元函数,B是C的友元函数,但是A不是C的友元函数
7.友元不宜多用。虽然又是提供了便利,但是会增加耦合度,破坏封装。
//友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator<<(istream& in, Date& d);
内部类
如果一个类定义在另一个类内部,这个类就是内部类。内部类是一个独立的类,与定义在全局想比,只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
内部类默认是外部类的友元类
内部类本质上也是一种封装,当A类和B类紧密关联,A类实现出来主要就是给B类使用的,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方用不了。
匿名对象
用类型(实参)定义出来的对象就叫做匿名对象,相比之下我们定义的类型对象名(实参)定义出来的叫有名对象。
匿名对象周期只在当前一行,一般临时定义一个对象当前用一个即可,就可以定义匿名对象。
匿名对象的一种应用。
#include<iostream>
using namespace std;
class A
{
public:A(int a = 0):_a(a){cout << "A(_a)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
class soultion
{
public:int Sum_Soulution(int n){return n;}
};
int main()
{A aa1;//这么定义对象不好,因为编译器无法世界下面是函数声明还是对象定义//我们可以这么定义匿名对象,不用给它取名字//但是生命周期只有这一行,下一行自动调用析构函数A();A(1);//匿名对象适用于一下场景,其他场景后续介绍soultion().Sum_Soulution(10);return 0;
}
对象拷贝时的编译器优化
现代编译器为了尽可能提高程序效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝
如何优化C++标准没有严格规定,哥哥编译器自行处理。目前主流相对的新编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更“激进”的编译器还会进行跨行跨表达式的合并优化。
Linux下可以将下列代码拷贝至test.cpp文件,编译时采用g++ test.cpp -fno-elide-constructors的方式关闭构造相关的优化。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class A
{
public:A(int a = 0):_a(a){cout << "A(int _a)" << endl;}A(const A& aa):_a1(aa._a1){cout<<"A(const A& aa)"<<endl;}A& operator=(const A& aa){cout<<"A& operator=(const A& aa)"<<endl;if (this != &aa){_a1 = aa._a1;}return *this;}~A(){cout<<"~A()"<<endl;}
private:int _a1=1;
};
void f1(A aa)
{
}
A d2()
{A aa;return aa;
}
class soultion
{int main()
{//传值传参//构造+拷贝构造A aa1;f1(aa1);cout << endl;//隐式类型,连续构造+拷贝构造->优化为直接构造f1(1);//一个表达式中,连续构造+拷贝构造->优化为一个构造f1(A(2));cout << endl;cout << "******************************************************" << endl;//传值返回//不优化下传值返回编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值//无优化(VS2019 Debug)f2();cout <<endl;//返回时一个表达式中,连续构造+拷贝构造->优化为一个构造(VS2019 Debug)//⼀些编译器会优化得更厉害,进⾏跨⾏合并优化。// 将构造的局部对象aa和拷⻉的临时对象和接收返回值对象aa2优化为⼀个直接构造。// (vs2022 debug)A aa2 = f2();cout << endl;//一个表达式中开始构造,中间拷贝构造+赋值重载->无法优化(VS2019 Debug)//⼀些编译器会优化得更厉害,进⾏跨⾏合并优化// 将构造的局部对象aa和拷⻉临时对象合并为⼀个直接构造(vs2022 debug)aa1 = f2();cout<< endl;return 0;
}
原理图如下所示:
感谢看到这里的读者朋友,希望您能够看在如此多的份量上,给我一个赞,谢谢