类中函数(分类,特殊成员或非成员函数)
类中函数(分类,特殊成员或非成员函数)
成员/方法访问控制
访问控制 | 客户通过类实例与句点 | 派生类中 | 类实现中 | 类实现的类方法中 | 场景 |
---|---|---|---|---|---|
private(默认) | 不能访问 | 不能访问 | 作用域解析运算符定义函数 | 可以访问 | 数据项一般放在私有部分 |
protected | 不能访问 | 可以访问 | 作用域解析运算符定义函数 | 可以访问 | |
public | 可以访问 | 可以访问 | 作用域解析运算符定义函数 | 可以访问 | 类接口放在公有部分、#define或const |
友元函数 | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
friend 友元函数【非成员函数】
简概
友元有三种
- 友元函数
- 友元类
- 友元成员函数
【功能扩展】友元函数 = 友元 x 函数
使用
原型:将原型放在类声明中,并在声明前加上关键字
friend
例如:
friend Time operator*(double m, const Time &t);
定义:不是成员函数,不加
类名::
的限定符
特点
- 友元函数在类声明中声明,但不是成员函数,即不能通过成员运算符来调用
- 虽然友元函数不是成员函数,但它和成员函数的访问权限相同
【实例】常用的友元:<<
- 这里要注意一下:通常重载<<运算符用于cout输出
- 该友元函数的返回值通常设置为
ostream &
,使之能进行连续的<<
运算符 - (这里之所以是返回引用类型,是为了让ostream的派生类都可用)
【实例】前缀
++
和后缀++
的写法- ......
两种重载运算符比较
函数版本 声明(类声明中) 定义(类实现中) 使用(客户代码中) 传参本质 普通函数 前面不加 friend
使用 类名::
指定类
只显式声明一个传入参数使用 成员运算符
调用一个操作数通过this指针隐式传递
一个显式传递成员函数版本 前面不加 friend
使用 类名::
指定类
只显式声明一个传入参数使用 运算符
隐式调用一个操作数通过this指针隐式传递
一个显式传递友元非成员函数版本 前面加 friend
不使用 类名::
指定类
显式声明两个传入参数
且其中一个为类的引用使用 运算符
隐式调用两个操作数都作为参数来传递
成员函数与非成员函数
成员函数简概
- 简概
- 包括构造函数和析构函数
- 与普通函数相比
- 定义上:先用作用域解析运算符指定类
- 性质上:可访问指定的类的私有成员
分类
类中函数一共两种:
- 非友元 - 类成员函数
- 构造函数
- 默认构造函数(自动提供)
- 复制构造函数(自动提供)
- 移动构造函数(C++11)
- 移动赋值函数(C++11)
- 其他构造函数
- 可单参数的构造函数
- 析构函数
- 默认析构函数(自动提供)
- 非友元的运算符重载函数
- 类型转换函数
- 普通成员函数
- 构造函数
- 友元 - 非成员函数
- 友元函数(包括友元的运算符重载函数)
非友元 - 非成员函数(类中不存在,是与类无关的普通函数)友元 - 成员函数(不存在,两者互斥)
区别
非友元 - 类成员函数
- 原型上:正常定义
- 定义上:先用作用域解析运算符指定类
- 性质上:可访问指定的类的私有成员
友元 - 非类成员函数
- 原型上:
friend
关键字开头 - 定义上:不用作用域解析运算符
- 性质上:可访问指定的类的私有成员
非友元 - 非类成员函数(普通函数)
- 略
【总结】成员函数属性(或友元)
注意:不全都是成员函数
函数 | 能否继承 | 成员还是友元 | 默认能否生成 | 能否为虚函数 | 是否可以有返回类型 |
---|---|---|---|---|---|
构造函数 | 否 | 成员 | 能 | 否 | 否 |
析构函数 | 否 | 成员 | 能 | 能 | 否 |
= | 否 | 成员 | 能 | 能 | 能 |
& | 能 | 任意 | 能 | 能 | 能 |
转换函数 | 能 | 成员 | 否 | 能 | 否 |
() | 能 | 成员 | 否 | 能 | 能 |
[] | 能 | 成员 | 否 | 能 | 能 |
-> | 能 | 成员 | 否 | 能 | 能 |
op= | 能 | 任意 | 否 | 能 | 能 |
new | 能 | 静态成员 | 否 | 否 | void * |
delete | 能 | 静态成员 | 否 | 否 | void |
其他运算符 | 能 | 任意 | 否 | 能 | 能 |
其他成员 | 能 | 成员 | 否不是 | 能 | 能 |
友元 | 否 | 友元 | 否 | 否 | 能 |
类中函数(具体函数)
构造与析构函数【成员函数】
构造函数(及其原理)
简概
- 简概
- 简单来说就是创建类实例的时候初始化
- 使用
- 没定义时有隐式版本的默认构造函数
- 定义:使用类名作为构造函数名(该函数名开头也大写),例如
Stock::Stock(...){...}
- 与普通成员函数相比
- 可默认声明
- 首字母大写并同类名(这种设计使得能用类名调用构造函数函数,而Python使用的是
__init__
标识构造函数名) - 没有返回值
- 默认还会被用于隐式转换
(成员变量注意项)
- 注意项
- 传输参数时,成员名和参数名不能相同,否则会出错(好像就只有C++会有这个问题)
- 解决方案:数据成员名使用
m_
前缀或使用_
后缀(有点类似于Python类设计中的双下划线标识和单下划线标识) - 能接受一个参数的构造函数,会被提供给自动/强制类型转换使用,也会被使用作
A a = val;
的初始化方式。这种行为可通过explicit
关闭
默认构造函数(自动提供)
- 要点
- 不提供任何构造函数时会创建默认构造函数,如果定义了构造函数将不会提供默认构造函数
- 也可自己显式地写默认构造函数
- 只能有一个默认构造函数,否则会引发二义性
- 原型
ClassName::ClassName();
- 调用场景
- 用于创建默认对象。如
A a;
- 用于创建默认对象。如
复制构造函数(自动提供)
- 要点
- 只能复制到新赋值的对象
- 赋值给新对象的对象应是对应类的引用(包括派生类)
- 赋值构造函数的功能是浅赋值
- 当成员中有动态创建的变量时这可能会引发一些问题(如
delete
一块堆内存两次,其症状之一是字符串出现乱码)
- 当成员中有动态创建的变量时这可能会引发一些问题(如
- 原型(注意是const引用)
ClassName(const ClassName &);
,如StringBad(const StringBad &);
- 调用场景
- 用于将一个对象复制到新创建的对象中,即只能用于初始化而不能用于其他赋值
- 具体场景(以类A为例,a1和a2都是类A的实例)
- 新对象初始化为一个同类对象
A a1(a2);
A a1 = a2;
A a1 = A(a2)
A * a1 = new A(a2)
- 按值将对象传递给函数
- 函数按值返回非引用对象
- 编译器生成临时对象
- ???
- 新对象初始化为一个同类对象
移动构造函数(C++11)
移动赋值函数(C++11)
析构函数(及其原理)
简概
- 简概
- 程序能跟踪对象,直到其过期为止
- 使用
- 没定义时有隐式版本的默认析构函数
- 定义:使用类名前加
~
作为析构函数名,例如Stock::~Stock(){}
- 调用:一般不能直接调用,使用定位new除外,写法例如
p_a.~A();
- 与普通成员函数相比
- 可默认声明
- 以
~
开头且首字母大写并同类名 - 没有返回值
- 不接受参数
- 会被自动调用(通常不显式调用,如果使用定义new运算符时手动调用可能会出错)
- 调用析构函数的情况(4种)
- 如果对象是动态变量,则执行完程序块后,自动调用析构函数
- 如果对象是静态变量,则程序结束时,自动调用析构函数
- 如果对象是new变量,则需要使用delete删除对象,间接自动调用析构函数
- 如果对象是非堆定位new变量,则不能使用delete,而应直接调用析构函数
默认析构函数(自动提供)
略,注意默认析构函数不会自动删除类中new创建的变量
【功能扩展】构造函数的扩展
【特殊语法】成员初始化列表 = 构造函数 x 成员变量初始化
使用
举例
Classy::Classy(int n, int m) :mem1(n),mem2(0),mem3(n*m+2) { // ... }
为什么效率高
对于简单变量来说没什么区别,但对于类对象成员来说,其效率更高
效率更高,但为什么效率高呢?
参考:【博客园】C++成员初始化列表作用,以及减少构造函数初始化步骤(这篇文章讲得非常好,推荐一看)
class B { public: explicit B(A a) // (1) 改成B(A a):m_a(a) // (2) 将该行改为 B(A& a):m_a(a) { cout << "////////////////////" << endl; cout << "a = " << &a << endl; cout << "m_a = " << &m_a << endl; m_a = a; } private: A m_a; // (2) 将该行改为 A& m_a; } int main() { A a{5}; B b{a}; return 0; } // 在这里,A的 Big3 一共调用了【4】次 // 1. A构造函数:创建 a // 2. A构造函数:创建 m_a // 3. A拷贝构造函数:B 构造函数形参构建 // 4. A的=号函数:为了给 m_a 赋值,还调用了一次赋值函数 // 【优化(1)】改成成员初始化列表 // 将第四点的赋值函数优化掉了,减少了一次 Big3 的使用。Big3 的数量为【3】 // 【优化(2)】Big3 的数量还能优化,将传参改成引用传参(指针传参也行) // 此时,只会调用一次A的构造函数。Big3 的数量为【1】
使用场景 / 必须使用的场景:
《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表
- 需要初始化的数据成员是对象的情况;
- 需要初始化const修饰的类成员;(不能赋值只能初始化)
- 需要初始化引用成员数据(不能“赋值”只能初始化)
// 这里仅列举情况一 class CMyClass { CMember m_member; public: CMyClass(); }; // 必须使用初始化列表来初始化成员 m_member CMyClass::CMyClass() : m_member(2) { ... }
杂项
局限性:这种格式只能用于构造函数
- 指的是函数后面初始化的语法格式,而不是指初始化变量的语法格式
- 后者可以用于常规初始化,如
int games(162);
、double talk(2.71828);
为什么叫 “成员初始化列表”,顾名思义,就是 成员变量 初始化 的列表
初始化顺序(大坑)
初始化的顺序为他们被声明的顺序,而不是他们在初始化列表中的顺序
如
A(int n1, int n2): an2(n1), an1(n2) {}
,如果在类中an1先被声明,则an1比an2先初始化class CMyClass { CMyClass(int x, int y); int m_x; int m_y; }; CMyClass::CMyClass(int i) : m_y(i), m_x(m_y) { } // 编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的 // 有两种方法避免它 // 一个是总是按照你希望它们被初始化的顺序来声明成员 // 第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
【特殊语法】类内初始化 = 构造函数 x 成员变量初始化
C++11中引入了类内初始化器,以减少构造函数和初始化代码的数量
使用
举例
class Classy { int n1 = 1; const int n2 = 2; // ... Classy(){} }
性质
- 这种写法==和使用成员初始化列表等价==
- 但如果同时使用,成员初始化列表会覆盖类内初始化
- 而且成员初始化列表能使用传入的参进行初始化,而类内初始化不行
类型转换函数【可隐式调用】【成员函数】
类型转化函数和运算符重载函数一样都使用 operator
关键字
能接受一个参数的构造函数(其他类型转换为类)
对象创建写法
A a = val;
本质是隐式类型转换的写法
自动/强制类型转换
- 描述:能接受一个参数的构造函数,也会被提供给自动和强制类型转换使用
- 好处:使用能接受单变量的构造函数来声明类型转换行为的好处:不用单独再写一种函数出来(比如转换函数)
转换函数(类转换为其他类型)
- 作用
- 将类自动或强制转换为其他类型
- 使用
例如
Stonewt::operator double()const;
例如:分数转化为浮点数
#include <iostream> using namespace std; class Fraction { public: Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {} operator double() const { // 分数转化为浮点数 return (double) m_numerator/m_denominator; } private: int m_numerator; // 分子 int m_denominator; // 分母 }; int main(void) { Fraction f(3, 5); double d = 3.2 + f; cout << d << endl; return 0; }
二义性问题
- 二步转换与二义性
- 比如给
Stonewt(double lbs);
赋值int类型时:会先将int转换为double,再进行转换 - 但仅当转换不存在二义性时才会进行二步转换,比如还定义了
Stonewt(long);
则编译器会报错,指出二义性
- 比如给
- 转换函数二义性
- 例如如果同时定义了类转换为int和转换为double两种转换函数,隐式转换会产生二义性
explicit 阻止隐式转换
- 作用
- 可通过关键字
explicit
关闭前面两种自动特性(单参数对象创建写法和自动类型转换) - 但仍允许显示强制类型转换
- 可通过关键字
- 使用
- 例如
explicit Stonewt(double lbs);
(构造函数) - 例如
explicit operator int() const;
(转换函数)
- 例如
- 关闭的隐式转换种类:(以
A(B b);
的构造函数为例)- 用B类型初始化A对象
- B类型赋值给A
- B值传递给接受A参数的函数
- 返回值为A的函数试图返回B值
- 历史版本
- C++98中,关键字
explicit
不能用于转换函数,但C++11消除了这种限制
- C++98中,关键字
运算符重载函数【可隐式调用】【成员函数】
类型转化函数和运算符重载函数一样都使用 operator
关键字
简概
- 听起来和函数重载差不多,但写法完完全全不同
- 类的多态的表现之一(类的多态的具体表现有:运算符重载、成员函数多态、构造函数多态、多态继承等)
使用
operator符号()
- 举例:
operator+()
,Time Time::operator+(const Time & t) const;
重载限制
重载限制
重载后的运算符必须至少有一个操作数是用户定义的类型
- 如:不能重载两个double值的和
不能违反运算符原来的句法规则
如:不能将求模运算符
%
重载成只使用一个操作数如:不能修改运算符的优先级
不能创建新的运算符
- 如:不能定义
operator**()
函数来表示求幂
- 如:不能定义
有的运算符不能重载,如下:
sizeof
,sizeof运算符.
,成员运算符.*
,成员指针运算符::
,作用域解析运算符?:
,条件运算符typeid
,一个RTTI运算符const_cast
,强制类型转换运算符dynamic_cast
,强制类型转换运算符reinterpret_cast
,强制类型转换运算符static_cast
,强制类型转换运算符
大多数运算符可以通过
成员
或非成员
函数进行重组,但有的运算符只能通过成员
函数重载,如下=
,赋值运算符()
,函数调用运算符[]
,下标运算符->
,通过指针访问类成员的运算符
可重载的运算符(表)
+
-
*
/
%
^
&
` ` ~=
!
=
>
+=
-=
*=
/=
%=
^=
&=
` =` <<
>>
<<=
==
!=
<=
>=
&&
` ` ++
--
,
()
[]
new
delete
new[]
delete[]
【缺陷补丁】友元函数
- 缺陷场景
- 比如能定义
classA * int
但不能定义int * classA
- 比如能定义
- 使用
- 略,详见功能扩展一节
捋一下
The Big Three
The Big Three,指 拷贝构造函数、赋值操作符、析构函数
- 如果类包含指针,必须要自己写拷贝构造,否则两个指针会指向同一个东西
- 当您需要编写其中任何一个时,您很可能需要编写另外两个。如果类包含指针,就必须手动实现这三种而非用默认给你的,否则会导致两个类
自动提供的成员函数【可隐式调用】(如果没有定义)
这里重点捋一下自动调用的场景
自动提供的成员函数:
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符
- 地址运算符
其中默认构造函数、默认析构函数、复制构造函数,前面都说过。这里来看下赋值运算符
赋值运算符(自动提供)
- 要点
- 赋值构造函数的功能是浅赋值
- 当成员中有动态创建的变量时这可能会引发一些问题(如
delete
一块堆内存两次,其症状之一是字符串出现乱码)
- 当成员中有动态创建的变量时这可能会引发一些问题(如
- 赋值构造函数的功能是浅赋值
- 原型
ClassName & Classname::operator=(const ClassName &);
- 调用场景
- 不要弄混赋值和初始化。若创建新的对象则使用初始化,若修改已有对象的值则使用赋值
- 同一个类的引用对象(包括派生类对象)赋值给该类的对象。如
a1 = a2;
- 用类的引用对象(包括派生类对象)初始化该类的对象时,一定调用复制构造函数,但不一定使用赋值运算符。如
A a1 = a2;
- 不一定是什么意思?详见前面
创建对象的n种方式
一节 - 解释器有两种方式去解释
A a1 = a2;
该语句:创建一个临时对象然后赋值的版本 才会使用赋值运算符
- 不一定是什么意思?详见前面
带来的一些问题(有new声明的成员时)
问题
- 默认的
复制构造函数
和复制运算符
使用的都是浅赋值 - 当成员中有动态创建的变量时这可能会引发一些问题(如
delete
一块堆内存两次,其症状之一是字符串出现乱码)
- 默认的
解决方案
- 详见前面的
【功能扩展】类成员 x new
一节
- 详见前面的
【区别】复制构造与重载=、构造与重载()
复制构造函数、重载运算符 =、转换函数
构造函数、重载运算符 ()
【功能扩展】类中函数
构造函数的扩展
【功能扩展】构造函数 x 函数重载/模板函数
【功能拓展】类模板 = 构造函数 x 模板
【功能拓展】STL = 类 x 泛型(C++标准模板库)
友元的扩展
- 使用场景:要让一个类访问令一个类的成员有两种途径:
- 让整个类成为另一个类的友元
- 让特定的类成员称为另一个类的友元(这种做法必须小心排列声明和定义的顺序)
【功能扩展】友元 x 类
- 写法
- 举例:
class TV { public: friend Remote; ...
- 顺序:可以先声明
class Tv
再声明class Remote
,也可以反过来(所以可以互相成为友元) - 顺序问题:这里先声明的Tv,但Remote不用向前声明是因为该友元语句本身已经指出Remote是一个类
- 举例:
- 扩展
- 可以让类彼此成为对方的友元
【功能扩展】友元 x 类成员
写法
- 举例:
class TV { public: friend void Remote::setTv(); ...
- 顺序:
class Tv;
->Remote的定义
->Tv的定义
(反过来不行,应在Tv前定义setTv方法) - 顺序问题:这里顺序有点麻烦:
- 编译器要能够处理这条语句,必须先知道Remote的定义,那应先声明Remote
- 而Remote中要访问Tv的成员,所以Tv应该声明在Remote前
- 这会形成
循环依赖
,解决方案:使用向前声明
(forward declaration)
- 举例:
顺序补充(谁向前声明)
参考:【CSDN】类的向前声明
CA.h
#ifndef HEADER_CA #define HEADER_CA #include "CB.h" class CA { CB* pB; CB b; //正确,因为此处已经知道CB类的大小,且定义了CB,可以为b分配空间 }; #endif
CB.h
#ifndef HEADER_CB #define HEADER_CB class CA;//这个必须要用,不能只用#include "CA.h",如果只是#include "CA.h"而没有class CA;则会报错. class CB { CA* pA; //CA a;//错误,因为此时还不知道CA的大小,无法分配空间 }; #endif
CA.cpp
#include "CA.h"
*.cpp文件只能#include “CA.h”.如果#icnlude “CB.h”则错误(展开后CA不识别CB)。
局限性
- 参考:【博客园】C++ 类的前向声明的用法
- 使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,在提供一个完整的类声明之前:
- 不能定义该类的对象 (因为此时编译器只知道这是个类,还不知道这个类的大小有多大)
- 不能在内联成员函数中使用该类的对象。 (因为此时根本不知道有些什么成员)
- 可以定义该类的指针、引用、以及用于函数形参的指针和引用