跳至主要內容

类中函数(分类,特殊成员或非成员函数)

LincZero大约 18 分钟

类中函数(分类,特殊成员或非成员函数)

成员/方法访问控制

访问控制客户通过类实例与句点派生类中类实现中类实现的类方法中场景
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++成员初始化列表作用,以及减少构造函数初始化步骤open in new window(这篇文章讲得非常好,推荐一看)

        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》中提到在以下三种情况下需要使用初始化成员列表

    1. 需要初始化的数据成员是对象的情况;
    2. 需要初始化const修饰的类成员;(不能赋值只能初始化)
    3. 需要初始化引用成员数据(不能“赋值”只能初始化)
    // 这里仅列举情况一
    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消除了这种限制

运算符重载函数【可隐式调用】【成员函数】

类型转化函数运算符重载函数一样都使用 operator 关键字

简概

  • 听起来和函数重载差不多,但写法完完全全不同
  • 类的多态的表现之一(类的多态的具体表现有:运算符重载、成员函数多态、构造函数多态、多态继承等)

使用

  • operator符号()
  • 举例:operator+()Time Time::operator+(const Time & t) const;

重载限制

重载限制

  • 重载后的运算符必须至少有一个操作数是用户定义的类型

    • 如:不能重载两个double值的和
  • 不能违反运算符原来的句法规则

    • 如:不能将求模运算符%重载成只使用一个操作数

    • 如:不能修改运算符的优先级

  • 不能创建新的运算符

    • 如:不能定义operator**()函数来表示求幂
  • 有的运算符不能重载,如下:

    • sizeof,sizeof运算符
    • .,成员运算符
    • .*,成员指针运算符
    • ::,作用域解析运算符
    • ?:,条件运算符
    • typeid,一个RTTI运算符
    • const_cast,强制类型转换运算符
    • dynamic_cast,强制类型转换运算符
    • reinterpret_cast,强制类型转换运算符
    • static_cast,强制类型转换运算符
  • 大多数运算符可以通过成员非成员函数进行重组,但有的运算符只能通过成员函数重载,如下

    • =,赋值运算符
    • (),函数调用运算符
    • [],下标运算符
    • ->,通过指针访问类成员的运算符

可重载的运算符(表)

  • +-*/%^
    &``~=!=
    >+=-=*=/=%=
    ^=&=`=`<<>>
    <<===!=<=>=&&
    ``++--,
    ()[]newdeletenew[]delete[]

【缺陷补丁】友元函数

  • 缺陷场景
    • 比如能定义classA * int但不能定义int * classA
  • 使用
    • 略,详见功能扩展一节

捋一下

The Big Three

The Big Threeopen in new window,指 拷贝构造函数、赋值操作符、析构函数

  • 如果类包含指针,必须要自己写拷贝构造,否则两个指针会指向同一个东西
  • 当您需要编写其中任何一个时,您很可能需要编写另外两个。如果类包含指针,就必须手动实现这三种而非用默认给你的,否则会导致两个类

自动提供的成员函数【可隐式调用】(如果没有定义)

这里重点捋一下自动调用的场景

自动提供的成员函数:

  • 默认构造函数
  • 默认析构函数
  • 复制构造函数
  • 赋值运算符
  • 地址运算符

其中默认构造函数、默认析构函数、复制构造函数,前面都说过。这里来看下赋值运算符

赋值运算符(自动提供)

  • 要点
    • 赋值构造函数的功能是浅赋值
      • 当成员中有动态创建的变量时这可能会引发一些问题(如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】类的向前声明open in new window

    • 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++ 类的前向声明的用法open in new window
    • 使用前向引用声明虽然可以解决一些问题,但它并不是万能的。需要注意的是,尽管使用了前向引用声明,在提供一个完整的类声明之前:
      • 不能定义该类的对象 (因为此时编译器只知道这是个类,还不知道这个类的大小有多大)
      • 不能在内联成员函数中使用该类的对象。 (因为此时根本不知道有些什么成员)
      • 可以定义该类的指针、引用、以及用于函数形参的指针和引用