C类和对象
【C++】类和对象
类的基本思想是 数据抽象 (
d a t a a b s t r a c t i o n data\ abstraction
d
a
t
a
ab
s
t
r
a
c
t
i
o
n )和 封装 (
e n c a p s u l a t i o n encapsulation
e
n
c
a
p
s
u
l
a
t
i
o
n )。数据抽象是一种依赖于 接口 (
i n t e r f a c e interface
in
t
er
f
a
ce )和 实现 (
i m p l e m e n t a t i n o implementatino
im
pl
e
m
e
n
t
a
t
in
o )分离的编程技术。对象是 类的实例 ,类是 对象的模板 。
一、类的定义
类 关键字 是
c l a s s class
c
l
a
ss ,定义一个类和定义一个结构体类似,但是类里面不仅可以放 变量 ,还可以放 函数 。
C++ 中
s t r u c t struct
s
t
r
u
c
t 也可以定义类, C++ 兼容 C 中
s t r u c t struct
s
t
r
u
c
t 的用法,同时
s t r u c t struct
s
t
r
u
c
t 升级成了类,明显的变化是
s t r u c t struct
s
t
r
u
c
t 中可以定义函数,一般情况下我们还是推荐用
c l a s s class
c
l
a
ss 定义类。
// 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; // 0
return 0;
}
1. 类的概念及定义
类中的内容称为 类的成员 :
- 类的 变量 称为类的 属性 或 成员变量 。
- 类的 函数 称为类的 方法 或 成员函数 。
注意:类定义结束时后⾯分号不能省略(和结构体类似)
例如,用 C++ 的类 封装 一些
S t a c k Stack
St
a
c
k 的简单功能:
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_year
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
这里主要是为了和初始化函数
I n i t ( ) Init()
I
ni
t
(
) 的形参变量作区分。(这里命名都希望使 各变量意义明确 ,因此才会命名冲突)
注意:定义在类里面的成员函数默认为
i n l i n e inline
in
l
in
e (声明和定义分离就不是内联了,定义函数的时候需要加域作用限定符)。
2. 访问限定符
C++ 一种实现 封装 的方式,用类将对象的 属性与方法结合 在⼀块,让对象更加完善,通过 访问权限 选择性的将其接口提供给外部的用户使用。
p u b l i c public
p
u
b
l
i
c
p u b l i c public
p
u
b
l
i
c 修饰的成员在类外可以 直接被访问 。
p r i v a t e private
p
r
i
v
a
t
e 和
p r o t e c t e d protected
p
ro
t
ec
t
e
d
p r o t e c t e d protected
p
ro
t
ec
t
e
d 和
p r i v a t e private
p
r
i
v
a
t
e 修饰的成员在类外 不能直接被访问 。(
p r o t e c t e d protected
p
ro
t
ec
t
e
d 和
p r i v a t e private
p
r
i
v
a
t
e 是一样的,以后 继承 章节才能体现出他们的区别)
一般 成员变量 都会被限制为
p r i v a t e / p r o t e c t e d private/protected
p
r
i
v
a
t
e
/
p
ro
t
ec
t
e
d ,需要给别人使用的 成员函数 才会放为
p u b l i c public
p
u
b
l
i
c 。
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
c
l
a
ss 默认 为
p r i v a t e private
p
r
i
v
a
t
e ,
s t r u c t struct
s
t
r
u
c
t 默认 为
p u b l i c public
p
u
b
l
i
c 。
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和d2
Date 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\ 地址]
[
c
a
ll
地址
] , 其实 编译器在编译链接时,就要找到函数的地址,不是在运行时找 ,只有动态多态是在运行时找,才需要存储函数的地址。
因此就有了第二种存储方式 —— 只存放类的成员变量 ,类的成员函数则放在 公共代码区 :
上面我们分析了 对象中只存储成员变量 ,C++规定类实例化的对象也要符合 内存对齐 的规则。
内存对齐规则:
第一个成员在结构体(类)偏移量为
0 0
0 的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数
= m i n ( 编译器默认对齐数 ( V S 默认对齐数为 8 ) , 该成员变量大小 ) 对齐数 == min(编译器默认对齐数(VS默认对齐数为8),该成员变量大小)
对齐数
==
min
(
编译器默认对齐数
(
V
S
默认对齐数为
8
)
,
该成员变量大小
)
结构体(类)总大小为:最大对齐数( m i n ( 编译器默认对齐数 , m a x { 所有变量类型 } ) )的整数倍。 结构体(类)总大小为:最大对齐数( min(编译器默认对齐数,max{所有变量类型})\ )的整数倍。
结构体(类)总大小为:最大对齐数(
min
(
编译器默认对齐数
,
ma
x
{
所有变量类型
})
)的整数倍。
如果嵌套了结构体(类)的情况 :
- 嵌套的结构体(类)对齐到自己的最大对齐数的整数倍处
- 结构体(类)的整体大小就是 所有最大对齐数 (含嵌套结构体(类)的对齐数) 的整数倍 。
// 计算⼀下A/B/C实例化的对象是多大?
//A:既有成员函数,又有成员变量
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private: //因此最大对齐数为:4 Byte(2个成员变量因此对象大小为:8)
char _ch; //大小为 1 Byte
int _i; //大小为 4 Byte
};
//B:只有成员函数
class B
{
public:
void Print()
{
//...
}
};
//C:空
class C
{
};
int main()
{
A a; B b; C c;
cout << sizeof(a) << endl; // 8
cout << sizeof(b) << endl; // 1
cout << sizeof(c) << endl; // 1
return 0;
}
运行结果为:
通过上述结果我们发现: 没有成员变量 的
B B
B 和
C C
C 类对象的大小是
1 1
1 。
为什么没有成员变量还要给 1 个字节呢?
因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了 占位标识对象存在 。
三、this 指针
既然类中只存储成员变量,成员函数都存放到了公共代码区,那么 我们在创建的不同对象要访问同一个函数的时候应该如何区分呢? 那么这里就要看到C++给了一个 隐含 的
t h i s this
t
hi
s 指针解决这里的问题。
这里我们还是用日期类来举例:
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
t
hi
s 指针。
在各个对象调用函数的时候会默认把当前对象的地址传给第一个参数。(这里用日期类定义的的对象
d 1 d_1
d
1
来举例:
d1.Init(&d1, 2025, 3, 9)
)
这样在函数调用的时候,每个函数都能通过
t h i s this
t
hi
s
指针来找到
对应对象的成员变量
。
this->_year = year;
注意: C++规定不能在 实参和形参 的位置显式地写
t h i s this
t
hi
s 指针(编译时编译器会处理),但是可以在 函数体内 显式使用
t h i s this
t
hi
s 指针。
有一个面试题这么考过:
Q : t h i s 指针存在内存哪个区域的( A ) Q:this指针存在内存哪个区域的 (\ A\ )
Q
:
t
hi
s
指针存在内存哪个区域的(
A
)
A . 栈 B . 堆 C . 静态区 D . 常量区 E . 对象里面 A. 栈\ \ B.堆\ \ C.静态区\ \ D.常量区\ \ E.对象里面
A
.
栈
B
.
堆
C
.
静态区
D
.
常量区
E
.
对象里面
因为
t h i s this
t
hi
s 指针实际上是 形参 ,因此会存在 函数栈帧 中,所以选
A A
A 是没问题的。(但是由于
t h i s this
t
hi
s 指针会频繁使用,
V S VS
V
S 编译器会将他存到 寄存器 中进行优化)
注意: 这里不能选
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
t
hi
s
==
n
u
llpt
r ),则不能访问成员变量。(会对
t h i s this
t
hi
s 空指针解引用 导致程序崩溃)
四、类的默认成员函数
默认成员函数就是用户没有显式实现, 编译器会自动生成的成员函数 称为 默认成员函数 。
一个类,我们不写的情况下 **编译器会默认生成以下
6 6
6 个默认成员函数** : 构造函数 、 析构函数 、 拷贝构造函数 、 赋值重载 、 普通对象取地址重载 和 **c o n s t const
co
n
s
t 对象取地址重载** 。(需要注意的是这
6 6
6 个中最重要的是前
4 4
4 个,最后两个取地址重载不重要,稍微了解一下即可)
C++11 以后还会增加两个默认成员函数: 移动构造 和 移动赋值 。
我们在学习默认成员函数的时候要始终怀揣着两个问题:
- 我们不写时,编译器 默认生成的函数行为是什么? 是否满足我们的需求?
- 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么 如何自己实现?
1. 构造函数
构造函数 (
C o n s t r u c t o r Constructor
C
o
n
s
t
r
u
c
t
or ) 的主要任务是对象实例化时初始化对象 ,而不是开空间创建对象。(我们常使用的局部对象是栈帧创建时空间就开好了)
构造函数的本质是要替代我们以前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/1
d2.Print(); // 2025/1/1
d3.Print(); // 2025/3/10
return 0;
}
构造函数的特点:
函数名与类名相同 。
无返回值 。(也不需要写
v o i d void
v
o
i
d )
对象实例化时,系统会 自动调用 对应的构造函数。
构造函数 可以重载 。(无参和全缺省构成函数重载,但调用时会产生歧义,因此不能同时存在)
如果类中没有显式定义构造函数,则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
co
n
s
t 修饰的 成员变量 。
被
c o n s t const
co
n
s
t 修饰的变量必须初始化,且初始化后不能再对其修改。
***注意:
c o n s t const
co
n
s
t 修饰的变量(成员变量)只能在初始化定义的时候赋值,其他时候不能对其更改。***
//error C2734: “x”: 如果不是外部的,则必须初始化常量对象
const int x;
const int y = 1;
//error C3892 : “y”: 不能给常量赋值
y = 2;
因此,如果被
c o n s t const
co
n
s
t 修饰的变量是成员变量的话,也必须初始化。所以就不能在函数体内初始化了(普通构造就不能用了),这个时候就只能用初始化列表去定义(必须在定义的时候就初始化)。
引用类型 (
& &
& )的 成员变量 。
引用类型的变量必须初始化。
//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
a
3 对象时,把右侧内置类型赋值给自定义类型( 类型冲突 ),因此需要进行(隐式) 类型转换 :
语法上: 构造⼀个
A A
A 的 临时对象 ,再用这个临时对象 拷贝构造
a 3 a3
a
3 。
实际上: 编译器遇到 连续构造
拷贝构造 -> 优化为 直接构造 。
***补充1:构造函数前面加
e x p l i c i t explicit
e
x
pl
i
c
i
t 就不再支持隐式类型转换:***
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
e
x
pl
i
c
i
t 修饰),运行直接报错:
补充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
Des
t
r
u
c
t
or )的主要任务是完成对象中资源的清理释放工作*** ,而不是完成对对象本身的销毁。(局部对象是存在栈帧的,函数结束栈帧销毁就释放了,不需要我们管)
析构函数的本质是要替代我们以前Stack类和Date类中写的Destroy()函数(销毁函数)的功能,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作,其完美的替代了Destroy()函数。
我们会发现
D a t e Date
D
a
t
e 类没有
D e s t r o y ( ) Destroy()
Des
t
roy
(
) 函数,其实就是没有资源需要释放(动态开辟的空间和指针变量),所以严格说
D a t e Date
D
a
t
e 类是不需要析构函数的。
因此,我们用
S t a c k Stack
St
a
c
k 和
M y Q u e u e MyQueue
M
y
Q
u
e
u
e 来举例:
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 st
MyQueue mq; //析构两次 Stack pushst、Stack popst
return 0;
}
析构函数的特点:
析构函数名是在类名前加上字符 ~ 。
无参数无返回值 。 (这里跟构造类似,也不需要加
v o i d void
v
o
i
d )
对象生命周期结束时,系统会 自动调用 析构函数。
一个类只能有一个析构函数 。(若未显式定义,系统会 自动生成默认的析构函数 )
编译器自动生成的析构函数对 内置类型 成员 不做处理 。
自定义类型 成员无论什么情况都会 自动调用析构函数 。
如上面写的
M y Q u e u e MyQueue
M
y
Q
u
e
u
e 类,运行结果如下:
因此对于自定义类型的成员变量不管写没写析构,都会自动调用成员变量的析构函数。
如果 类中没有申请资源或者默认生成的析构可以用时,析构函数可以不写 ,直接使用编译器生成的默认析构函数即可,如
D a t e Date
D
a
t
e 和
M y Q u e u e MyQueue
M
y
Q
u
e
u
e ;
如果 有资源申请时,一定要自己写析构,否则会造成资源泄漏 ,如
S t a c k Stack
St
a
c
k 。
- 一个局部域的多个对象,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
d
1 初始化了
d 2 d2
d
2 )。
拷贝构造的特点:
- 拷贝构造函数是 构造函数的一个重载 。
- 拷贝构造函数的 第一个参数必须是类类型对象的引用 ,如果有多个参数, 后面的参数必须有缺省值 。
使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。
- 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
D
a
t
e 这样的类成员变量 全是内置类型 且 没有指向什么资源 ,编译器自动生成的拷贝构造就可以完成需要拷贝( 浅拷贝 ),所以不需要我们显式实现拷贝构造。
浅拷贝:一个字节一个字节地拷贝。
而像
S t a c k Stack
St
a
c
k 这样的类,虽然也都是 内置类型 ,但是
_ 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
s
t
1 和
s t 2 st2
s
t
2 里的
_ a _a
_
a 指针指向同一块空间, 析构时会析构两次 (
f r e e free
f
ree 了两次),程序直接就崩溃了:
这时候会发现,把析构函数 ~
S t a c k ( ) Stack()
St
a
c
k
(
) 删了就不会报错了。(但是这样会 内存泄漏 啊!!!不能这样干)
因此我们这个时候就 必须自己写拷贝构造函数 了:
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
M
y
Q
u
e
u
e 这样的类型内部主要是 自定义类型
S t a c k Stack
St
a
c
k 成员,编译器自动生成的拷贝构造会调用
S t a c k Stack
St
a
c
k 的拷贝构造,也不需要我们显式地实现
M y Q u e u e MyQueue
M
y
Q
u
e
u
e 的拷贝构造。
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
St
a
c
k 和两个 拷贝构造 的
S t a c k Stack
St
a
c
k 。
这里有一个小技巧:
如果一个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则就不需要。
P.S. 拷贝构造可以写成两种形式:
1. Stack st2(st1);
2. Stack st2 = st1;
这两种写法是等价的,都代表调用st1的拷贝构造函数,将结果拷贝给st2。
传值返回会产生一个临时对象调用拷贝构造 。
传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝 。
Stack func()
{
Stack st;
return st; //调用Stack的拷贝构造
}
int main()
{
Stack ret = func(); //将拷贝构造的值返回给ret
return 0;
}
( 注意: 如果返回对象是一个当前 函数局部域的局部对象 ,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个 野引用 ,类似一个野指针一样 )
Stack& func()
{
Stack st; //函数内部定义的局部变量出函数就会被销毁
//warning C4172: 返回局部变量的地址或临时 : st
return st; //返回st被销毁因此返回了一个野引用
}
int main()
{
Stack ret = func(); //此时ret为野引用(一旦调用就会报错)
ret.Push(1); //realloc failed!
return 0;
}
运行结果:
这里因为对野引用进行操作所以就直接报错了。 因此我们传引用返回的时候,要保证对象没有被销毁。
因此我们可以在函数内部定义的对象加上
s t a t i c static
s
t
a
t
i
c 让他变为 静态全局的,延长它的生命周期 ;还可以把对象在函数外定义好了 传参给函数 ,这样就不怕出了函数作用域对象销毁的问题了。
传引用返回可以减少拷贝 ,但是一定要确保返回对象,在当前函数结束后还在,( 保证不是野引用 )才能用引用返回。
4. 赋值运算符重载
4.1 运算符重载
当运算符被用于 类类型 的对象时,C++语言允许我们通过 运算符重载 的形式指定新的含义。
运算符重载 是一个特殊的 函数 ,他的名字是由
o p e r a t o r operator
o
p
er
a
t
or 和后面要定义的 运算符 共同构成。
( 返回类型 ) o p e r a t o r ( 运算符 ) ( 参数 1 , 参数 2 ) { 函数体 } (返回类型)\ operator(运算符)\ (参数1,参数2) {函数体}
(
返回类型
)
o
p
er
a
t
or
(
运算符
)
(
参数
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
t
hi
s 指针** ,因此运算符重载作为成员函数时,参数比运算对象少一个(实际上少的那一个变成隐含的了)。
如果全局和成员都写了运算符重载,会优先调用成员函数的运算符重载。(先找类域,再找全局)
当
o p e r a t o r operator
o
p
er
a
t
or 定义为全局函数时,由于类的成员变量都是私有的,因此
o p e r a t o r operator
o
p
er
a
t
or 不能直接访问其成员变量:
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;
else
cout << "不等于" << endl;
return 0;
}
这里是把是直接把
p r i v a t e : private:
p
r
i
v
a
t
e
: 给注释掉了,即把成员变量先置为公有,让
o p e r a t o r operator
o
p
er
a
t
or
能够访问到,不然会直接报错:
error C2248: “Date::_year”: 无法访问 private 成员(在“Date”类中声明)
。当然也可以像
J a v a Java
J
a
v
a 一样搞一个
G e t ( ) Get()
G
e
t
(
) 函数,让成员变量变成只读。
但是我们还可以直接把
o p e r a t o r operator
o
p
er
a
t
or 函数直接放到类内部,这样就能直接访问:
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;
else
cout << "不等于" << endl;
return 0;
}
这里直接把
o p e r a t o r operator
o
p
er
a
t
or
函数复制粘贴到类里面,运行后会发现直接报错了:
error C2804: 二进制“operator ==”的参数太多
。
之前我们在学习
t h i s this
t
hi
s
指针的时候,知道成员函数默认都会在
形参第一个位置
,增加一个
当前类类型
的指针
Date* const this
,叫做
t h i s this
t
hi
s 指针。
因此,
o p e r a t o r operator
o
p
er
a
t
or 做成员变量的时候,第一个参数会默认为当前对象的
t h i s this
t
hi
s 指针(隐藏起来了),这个指针访问的是 当前类的成员变量 即 第一个对象 的成员变量,这时只需要传一个参数(代表右侧运算对象)即可。
总结:运算符重载作为成员函数时,参数比运算对象少一个。 **(第一个参数为
t h i s this
t
hi
s 指针)**
因此修改后的
o p e r a t o r
= ( ) operator == ()
o
p
er
a
t
or
==
(
) 函数应该只有一个参数:
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;
else
cout << "不等于" << endl;
return 0;
}
归纳总结:重载为全局的面临对象访问私有成员变量 的问题,有几种方法可以解决:
成员放公有。
**D a t e Date
D
a
t
e 提供
g e t x x x getxxx
g
e
t
xxx 函数。**
友元函数。
重载为成员函数。
运算符重载以后,其 优先级 和 结合性 与对应的内置类型运算符保持一致。
不能 通过连接语法中没有的符号来 创建 新的操作符:比如
o p e r a t o r operator
o
p
er
a
t
or @。
注意:以下五个运算符不能重载: (1)
. ∗ .*
.
∗ (2)
: : ::
:: (3)
s i z e o f sizeof
s
i
zeo
f (4)
? : ?:
?
: (5)
. .
. ( 面试题常考 )。
重载操作符至少有一个 类类型 参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x, int y)
。一个类需要重载哪些运算符,是看哪些运算符重载后 有意义 ,比如
D a t e Date
D
a
t
e 类重载
o p e r a t o r − operator-
o
p
er
a
t
or
− 就有意义(中间多少天),但是重载
o p e r a t o r + operator+
o
p
er
a
t
or
就没有意义。
重载
++
运算符时,有 前置++ 和 后置++ ,运算符重载函数名都是
o p e r a t o r + + operator++
o
p
er
a
t
or
,无法很好的区分。
***C++规定:后置++重载时,增加⼀个
i n t int
in
t 形参,跟前置++构成函数重载,方便区分。***
//前置++/--
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
t
hi
s 指针 默认抢占了第一个形参位置 ,第一个形参位置是左侧运算对象,调用时就变成了
对象 < < c o u t 对象«cout
对象
«
co
u
t ,不符合使用习惯和可读性)
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
co
u
t
«
d 会直接报错,因为参数位置不匹配:
因为成员函数的第一个参数永远是
t h i s this
t
hi
s 指针,因此会把
c o u t cout
co
u
t 挤到第二个参数去。
于是写成
d < < c o u t d « cout
d
«
co
u
t 就不会报错了:
写成这样虽然正确,但用起来就有点 倒反天罡 的意思了。
***因此,我们可以重载为全局函数,把
o s t r e a m / i s t r e a m ostream/istream
os
t
re
am
/
i
s
t
re
am 放到第一个形参位置就可以了,第二个形参位置当类类型对象:***
***注意:流对象不支持拷贝构造,因此调用时必须用引用:比如调用
c o u t cout
co
u
t 时用
o s t r e a m & ostream&
os
t
re
am
& 。***
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«
o
p
er
a
t
or
« 在重载为 全局函数 之后,便对
D a t e Date
D
a
t
e 的成员变量 失去了访问权 (私有成员)。因此在
D a t e Date
D
a
t
e 类中对
o p e r a t o r < < operator«
o
p
er
a
t
or
« 函数加上一个 友元声明 。
这个时候还有一个问题, 怎么解决连续输出的问题呢? —— 答案是: **可以增加一个返回值
c o u t cout
co
u
t** 。
再更改一下上面的程序,整理一下就得到了一个比较完善的流提取的运算符重载函数:
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
co
n
s
t 当前类类型引用 ,否则会传值传参会有拷贝。
有返回值 ,且 建议 写成 当前类类型引用 ,引用返回可以提高效率,有返回值目的是为了 支持连续赋值 场景。
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=
o
p
er
a
t
or
= 是全局函数,成员函数的
o p e r a t o r
operator=
o
p
er
a
t
or
= 第一个参数是
t h i s this
t
hi
s 指针,应该返回啥呢?
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)的返回值是d1
cout << endl;
cout << "d1:"; d1.Print();
cout << "d2:"; d2.Print();
cout << "d3:"; d3.Print();
return 0;
}
运行结果为:
可以看出,赋值重载给了返回值就可以进行连续赋值操作。
- 没有显式实现时, 编译器会自动生成一个默认赋值运算符重载 ,默认赋值运算符重载行为跟默认拷贝构造函数类似,对 内置类型 成员变量会完成 值拷贝/浅拷贝 (一个字节一个字节的拷贝),对 自定义类型 成员变量会调用他的 赋值重载 函数。
这里和拷贝构造非常像:
像
D a t e Date
D
a
t
e 这样的类成员变量 全是内置类型 且 没有指向什么资源 ,编译器自动生成的赋值运算符重载就可以完成需要的拷贝( 浅拷贝 ),所以不需要我们显式实现赋值运算符重载。
像
S t a c k Stack
St
a
c
k 这样的类,虽然也都是 内置类型 ,但是
_ a _a
_
a 指向了资源 ,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现 深拷贝 (对指向的资源也进行拷贝)。
像
M y Q u e u e MyQueue
M
y
Q
u
e
u
e 这样的类型内部主要是 自定义类型
S t a c k Stack
St
a
c
k 成员,编译器自动生成的赋值运算符重载会调用
S t a c k Stack
St
a
c
k 的赋值运算符重载,也不需要我们显式实现
M y Q u e u e MyQueue
M
y
Q
u
e
u
e 的赋值运算符重载。
这里还有一个小技巧:
如果一个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载,否则就不需要。
5. 取地址运算符重载
我们在定义对象的时候,不仅会定义 普通对象 ,也可能会定义 **c o n s t const
co
n
s
t 对象** 。
以日期类来举例:
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
co
n
s
t 对象**
d 2 d2
d
2 如果想要访问其成员函数的话,会造成 权限放大 问题:
这里的
d 2 d2
d
2 传的
t h i s this
t
hi
s 指针是
D a t e ∗ Date^*
D
a
t
e
∗ 类型的(忘了的话可以翻翻上面第三章关于
t h i s this
t
hi
s 指针的内容),因此在语法层面上来讲,在成员函数里并没有对其限定不可修改,如果我想修改也是可以修改的:
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 this
return 0;
}
注意:这里Date* const this里的const是指定this不能修改,而不是指定this所指向的空间(Date类)不能修改。
完全可以把对象信息给改了:
所以,如果我们定义的对象是
c o n s t const
co
n
s
t 对象,那么传到成员函数的参数
t h i s this
t
hi
s 指针却是
D a t e ∗ c o n s t t h i s Date^*\ const\ this
D
a
t
e
∗
co
n
s
t
t
hi
s ,我们理应应当把其改为
c o n s t D a t e ∗ c o n s t t h i s const\ Date^*\ const\ this
co
n
s
t
D
a
t
e
∗
co
n
s
t
t
hi
s 类型,才能确保对象受
c o n s t const
co
n
s
t 保护,在函数中也就不能随便修改了。
***那么怎么在成员函数加上
c o n s t const
co
n
s
t 呢?在哪加呢?*** —— 我们引入了
c o n s t const
co
n
s
t 成员函数。
5.1 const 成员函数
**将
c o n s t const
co
n
s
t 修饰的成员函数称之为
c o n s t const
co
n
s
t 成员函数** ,
c o n s t const
co
n
s
t 修饰成员函数放到 成员函数参数列表的后面 。
以
D a t e Date
D
a
t
e 类里的
P r i n t ( ) Print()
P
r
in
t
(
) 函数举例:
c o n s t const
co
n
s
t 修饰
D a t e Date
D
a
t
e 类的
P r i n t ( ) Print()
P
r
in
t
(
) 成员函数,
P r i n t ( ) Print()
P
r
in
t
(
) 隐含的
t h i s this
t
hi
s 指针由
D a t e ∗ c o n s t t h i s Date^*\ const\ this
D
a
t
e
∗
co
n
s
t
t
hi
s 要变为
c o n s t D a t e ∗ c o n s t t h i s const\ Date^*\ const\ this
co
n
s
t
D
a
t
e
∗
co
n
s
t
t
hi
s 才能保证对象不被修改(权限匹配),而由于
t h i s this
t
hi
s 指针是隐含的,没有办法直接在参数前加上
c o n s t const
co
n
s
t 。
因此,规定成员函数
v o i d P r i n t ( ) { . . . } void\ Print()\ {…}
v
o
i
d
P
r
in
t
(
)
{
…
} 用
c o n s t const
co
n
s
t 修饰后写成
v o i d P r i n t ( ) c o n s t { . . . } void\ Print()\ const\ {…}
v
o
i
d
P
r
in
t
(
)
co
n
s
t
{
…
} 。
***c o n s t const
co
n
s
t 实际修饰该成员函数隐含的
t h i s this
t
hi
s 指针,表明在该成员函数中不能对类的任何成员进行修改*** 。
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
d
2 :
且如果修改了对象的内容会直接报错:
***总结:只要不修改对象的成员函数都可以加
c o n s t const
co
n
s
t ,加上
c o n s t const
co
n
s
t 肯定更安全。***
5.2 取地址运算符重载
取地址运算符重载分为 普通取地址运算符重载 和 **c o n s t const
co
n
s
t 取地址运算符重载** 。
一般这两个函数编译器自动生成的就可以够我们用了,不需要我们去显式实现。(默认成员函数)
除非一些很特殊的场景:比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。(可见也没啥真正使实用的应用场景)
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^*
D
a
t
e
∗ 。
c o n s t const
co
n
s
t 取地址运算符重载:需要返回
c o n s t D a t e ∗ const\ Date^*
co
n
s
t
D
a
t
e
∗
注意:虽然普通对象也可以访问const取地址重载函数,但返回的是const Date*就不合理,因此要写两个。
函数实现直接返回
t h i s this
t
hi
s 指针(本身就是指向对象的地址)即可,根本不用写什么东西,编译器都实现好了。
五、static 成员
⽤
s t a t i c static
s
t
a
t
i
c 修饰的成员变量,称之为 静态成员变量 ,静态成员变量一定要在 类外进行初始化 。(但是要 在类内声明 :因为 静态成员也是类的成员 ,因此也受
p u b l i c public
p
u
b
l
i
c 、
p r o t e c t e d protected
p
ro
t
ec
t
e
d 、
p r i v a t e private
p
r
i
v
a
t
e 访问限定符的限制 )
注意:这里静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
注意:静态成员变量为类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
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
s
t
a
t
i
c 修饰的成员函数,称之为 静态成员函数 ,静态成员函数 **没有
t h i s this
t
hi
s 指针** 。
因此,
静态成员函数 中 可以访问其他的静态成员 ,但是 不能访问非静态的 ,因为没有
t h i s this
t
hi
s 指针。
非静态的成员函数 , 可以访问任意 的静态成员变量和非静态成员函数,因为有
t h i s this
t
hi
s 指针。
也就是说, ***静态成员是大家公有的、共享的;而非静态成员只能通过
t h i s this
t
hi
s 指针来访问。***
突破类域就可以访问静态成员:
可以通过
类名 : : 静态成员 类名::静态成员
类名
::
静态成员 或者
对象 . 静态成员 对象.静态成员
对象
.
静态成员 来访问 静态成员变量 和 静态成员函数 。
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的值:10
cout << A::Get_a1() << endl; //1.类名::静态成员
cout << a.Get_a1() << endl; //2.对象.静态成员
return 0;
}
***那么
s t a t i c static
s
t
a
t
i
c 成员具体有啥应用呢?***
提出一个问题: 实现一个类,计算程序中创建出了多少个类对象?
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
s
t
a
t
i
c 成员确实都是共享的,每个对象都一样。
通过上面这段程序,我们就可以用这种方法做下面这道题:
【题目信息】
[【剑指
o f f e r offer
o
ff
er 】
求
1 + 2 + 3 + . . . + n 1+2+3+…+n
1
2
3
…
n :]( )
这是 **《剑指
o f f e r offer
o
ff
er 》** 上的一道题,虽然题目很简单,但是给了很多条件限制,因此常规方法基本上是不能用的,所以我们要 另辟蹊径 —— 用 非常规方法 :
【题目解析】
这道题的意思是只能用
、 − +、-
、
− 来实现累加了,那么找规律发现一共有
n n
n 项,从第
1 1
1 项到第
n n
n 项,每一项都是上一项
++
后的结果,也就是说可以定义一个
i i
i 变量代表每一个元素,加到一个初始值为
0 0
0 的
s u m sum
s
u
m 变量中,代表累加和,每次加完就让
i + + i++
i
,一共加上
n n
n 次就完美的解决了问题。
***现在的问题是:怎么不用循环累加
n n
n 次呢?***
这看似有些难为人,但可以发现,我们上面的程序可以计算程序中创建出了多少个类对象,也就是说, 创建多少次对象,就会执行多少次构造函数 。
因此,我们可以将
i i
i 和
s u m sum
s
u
m 定义成静态变量,放到构造函数里去累加,这样如果创建一个个数为
n n
n 的
S u m Sum
S
u
m 类类型数组,即创建
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
f
r
i
e
n
d ,并且 把友元声明放到一个类的里面 。
1. 友元函数:
外部友元函数 **可访问类的私有(
p r i v a t e private
p
r
i
v
a
t
e )和保护(
p r o t e c t e d protected
p
ro
t
ec
t
e
d )成员** , 友元函数仅仅是一种声明,他不是类的成员函数。
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:123
return 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
p
r
i
v
a
t
e
/
p
ro
t
ec
t
e
d 位置,那么
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
L
in
ux 下可以将下面代码拷贝到
t e s t . c p p test.cpp
t
es
t
.
c
pp 文件,编译时用
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
St
a
c
k 形态上还是发生了挺多的变化,底层和逻辑上没啥变化。(只是进行了 封装 )
- 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 1
ps->top = 0;
#endif
//2.top指向栈顶元素
#if 0
ps->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()
I
ni
t
(
) 函数给的缺省参数会方便很多。
成员函数每次不需要传对象地址,因为
t h i s this
t
hi
s 指针隐含的传递了,方便了很多。
使用类型不再需要
t y p e d e f typedef
t
y
p
e
d
e
f ,直接用类名就很方便。
这只是 C++ 入门阶段实现的
S t a c k Stack
St
a
c
k 看起来变了很多,但是实质上变化不大。等后面看
S T L STL
ST
L 中的用 适配器 实现的
S t a c k Stack
St
a
c
k ,就能够感受到 C++ 的魅力。
2. 日期类的实现(运算符重载)
实现一个日期类主要是为了 练习运算符重载的使用 ,使一个 自定义类型 能够进行和 内置类型 类似的 加 、 减 、 比较大小 和 输入输出 等运算符操作,同时也能学会 日期类问题 的解决思路。
我们在实现一个项目(类)时,要做到声明和定义分离,养成一个好习惯,因此我们把日期类总共分为了三个文件:
d a t e . h date.h
d
a
t
e
.
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
d
a
t
e
.
c
pp 用来存放 日期类方法的实现 ;
#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
t
es
t
.
c
pp 文件用来 测试 我们写的日期类对不对,是否能达到我们的需求。
#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-d2
test_6(); //测试>>、<<
return 0;
}
总结
以上就是对 C++ 中类和对象部分的大总结,这篇博客我前前后后码了差不多有一个周,全文差不多有
4.5 w 4.5w
4.5
w 字左右,希望能对你有所帮助。