Skip to content

1. 友元函数和运算符重载

1.1 友元函数

友元(friend):在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问。这种操作可以实现很多操作,利用友元可以很方便的实现这种操作。友元是对类封装机制的一种补充。

在函数声明前加 friend,就变成了友元函数。

  • 普通的全局函数作为类的友元函数
    • 友元函数是某些不是类的成员函数却能够访问类的所有成员函数,类授予它特别的访问权,这样该友元函数就能够访问类中的所有成员
    • 友元函数的声明
      • 在授权类中声明访问权限
      • 格式:friend 返回值类型 函数名(形参列表);

示例:

cpp
#include<iostream>
using namespace std;

class A
{
public:
    void memberFun();
    friend void globalFun_friend(const A& that);
    A(int, int);

private:
    int m_private;
protected:
    int m_protected;
};

void globalFun(const A& that);

void A::memberFun()
{
    cout << "成员函数" << endl;
    cout << "m_private = " << m_private << ", m_protected = " << m_protected << endl;
}

A::A(int r, int i) : m_private(r), m_protected(i)
{
    cout << "构造函数" << endl;
    cout << "m_private = " << m_private << ", m_protected = " << m_protected << endl;
}

void globalFun_friend(const A& that)
{
    cout << "友元函数" << endl;
    cout << "m_private = " << that.m_private << ", m_protected = " << that.m_protected << endl;
}

void globalFun(const A& that) // 非友元函数,私有成员不对其开放
{
    cout << "全局函数" << endl;
    // cout << "m_private = " << that.m_private << ", m_protected = " << that.m_protected << endl; // error
}

int main()
{
    A a(1, 2);
    cout << endl;
    a.memberFun();
    cout << endl;
    // a.globalFun_friend(a); // error: globalFun_friend不是A类成员,不能这样访问
    globalFun_friend(a);
    cout << endl;
    globalFun(a);
    return 0;
}

1.2 运算符重载

重载: 在同一个作用域内定义具有相同名称但参数列表不同的函数或方法。通过重载,可以根据不同的参数类型或参数个数来区分和调用不同的函数。

运算符重载是 C++ 中的一个特性,它允许用户自定义已定义的运算符的行为,使其适用于自定义的类和数据类型。通过运算符重载,可以使用自定义的语义和操作来处理类对象。当我们说“重载运算符”时,我们的意思是改变某个运算符对用户自定义类型的行为。

1.2.1 双目运算符的重载

运算符的重载本质就是写一个函数满足相应的需求。

步骤:

  • 左值和右值的本质就是该参数或返回值是否会被修改
  • 确定要进行重载的操作符
  • 确定函数的参数要求(左值还是右值,需要修改还是只读)
  • 确定函数的返回值要求(左值还是右值)
  • 函数是否需要将对象调用

C++ 的语法规则将一些运算符(如 =[]()-> 等)限制为只能被定义为成员函数,而不能作为全局函数进行重载。这是为了确保运算符重载可以在正确的上下文中使用,并且能够访问所需的对象状态。

1.2.1.1 运算类的双目运算符

常见的运算类的双目运算符有 +-*/

示例:

cpp
int a = 1;
int b = 2;
int c = a + b;

特点:

  • 左右操作数既可以是左值也可以是右值
  • 表达式结果是右值

实现方式:

  • 成员函数方式
    L#R 的表达式可以被编译器翻译成 L.operator#(R) 这样成员函数调用形式,该函数的返回结果就是表达式的结果。
    其中 L 表示左操作数、R 表示右操作数、# 表示运算符,L 必须是类类型变量,才可调用 operator# 函数。operator 是一个特殊的关键字,用于定义或重载运算符。

  • 全局函数形式
    L#R 的表达式可以被编译器翻译成 ::operator#(L, R) 这样成员函数调用形式,该函数的返回结果就是表达式的结果。
    其中 L 表示左操作数、R 表示右操作数,LR 至少有一个是类类型的变量。

复数的加减运算示例:

cpp
#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(int r, int i) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << "+" << m_i << "i" << endl;
    }
    // c1 + c2 ==> c1.operator+(c2)
    /*
    * 1) 修饰返回值, 为了返回右值 // 防止出现c1 + c2 = c3; 这种现象, 只能出现c3 = c1 + c2
    * 2) 常引用, 为了支持常量类型型右操作数(右值) // 当c2为常数时是可以运算
    * 3) 常函数, 为了支持常对象调用该函数 // 当c1为常数时是可以运算
    */
    const Complex operator+(const Complex& c) const
    {
        Complex res(m_r + c.m_r, m_i + c.m_i);
        return res;
    }
private:
    int m_r; // 实部
    int m_i; // 虚部
    // 友元函数可以访问类中的私有成员
    friend const Complex operator-(const Complex& l, const Complex& r);
};

// c1 - c2 ==> ::operator#(L, R)
/*
* 1) 修饰返回值, 为了返回右值 // 防止出现c1 + c2 = c3; 这种现象, 只能出现c3 = c1 + c2
* 2) 常引用, 为了支持常型右操作数(右值) // 当c2为常对象时是可以运算
* 3) 常引用, 为了支持常型右操作数(右值) // 当c2为常对象时是可以运算
*/
const Complex operator-(const Complex& l, const Complex& r)
{
    Complex res(l.m_r - r.m_r, l.m_i - r.m_i);
    return res;
}

int main()
{
    /*const*/ Complex c1(1, 2);
    /*const*/ Complex c2(3, 4);
    c1.print();
    c2.print();
    // c1.operator+(c2)
    Complex c3 = c1 + c2;
    c3.print(); // 4+6i
    // ::operator-(c2, c1);
    c3 = c2 - c1;
    c3.print(); // 2+2i
    return 0;
}
1.2.1.2 赋值类的双目运算符

常见的如 +=-=*=/==

示例:

cpp
int a = 1;
int b = 2;
a += b;

特点:

  • 左操作数必须是左值,右操作数可以是左值也可以是右值
  • 表达式的结果是左值,就是左操作数的自身

实现方式:

  • 成员函数形式
    L#R 的表达式可以被编译器翻译成 L.operator#(R) 这样成员函数调用形式,该函数的返回结果就是左操作数。
  • 全局函数形式
    L#R 的表达式可以被编译器翻译成 ::operator#(L, R) 这样成员函数调用形式,该函数的返回结果就是左操作数。

示例:

cpp
#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(int r, int i) : m_r(r), m_i(i) {}
    void print(void) const
    {
        cout << m_r << "+" << m_i << "i" << endl;
    }
    // += 成员函数形式
    // 返回值是左值,不加 const
    // 左操作数必须是左值,因此不能是常函数
    Complex& operator+=(const Complex& c)
    {
        m_r += c.m_r;
        m_i += c.m_i;
        return *this;
    }
    Complex& operator=(const Complex& that) // 拷贝赋值
    {
        if (this == &that)
        {
            return *this;
        }
        m_r = that.m_r;
        m_i = that.m_i;
        return *this;
    }
private:
    int m_r; // 实部
    int m_i; // 虚部
    // 友元函数可以把定义直接写在类的内部, 但是它不属于类, 本质还是全局函数
    // 左操作数必须是左值, 因此不能加 const
    friend Complex& operator-=(Complex& l, const Complex& r)
    {
        l.m_r -= r.m_r;
        l.m_i -= r.m_i;
        return l;
    }
};

int main()
{
    Complex c1(1, 2);
    Complex c2(3, 4);
    c1 += c2; // c1.operator+=(c2)
    c1.print(); // 4+6i
    c1 -= c2; // ::operator-=(c1, c2)
    c1.print(); // 1+2i

    Complex c3(5, 6);
    c3 = c2;
    c3.print(); // 3+4i
    return 0;
}
1.2.1.3 比较类的双目运算符

常见的如 <><=>===

示例:

cpp
int a = 1;
int b = 2;
bool c = (a > b); // false

特点:

  • 左、右操作数既可以是左值也可以是右值
  • 表达式的结果是布尔值

实现方式:

  • 成员函数形式
    L#R 的表达式可以被编译器翻译成 L.operator#(R) 这样成员函数调用形式
  • 全局函数形式
    L#R 的表达式可以被编译器翻译成 ::operator#(L, R) 这样成员函数调用形式

示例:

cpp
#include<iostream>
using namespace std;

class Int
{
public:
    Int(int data = 0) : m_data(data)
    {
    }
    bool operator>(const Int& i)
    {
        if (this->m_data > i.m_data)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    friend bool operator==(const Int&, const Int&);
    void print()
    {
        cout << "m_data = " << m_data << endl;
    }
private:
    int m_data;
};

bool operator==(const Int& i1, const Int& i2)
{
    if (i1.m_data == i2.m_data)
    {
        return true;
    }
    else
    {
        return false;
    }
}

int main()
{
    Int i1(20);
    Int i2(10);

    cout << (i1 > i2) << endl; // true
    cout << (i1 == i2) << endl; // false

    return 0;
}
1.2.2 单目运算符的重载
1.2.2.1 计算类的单目操作符

例如:-~

示例:

cpp
int a = 1;
int b = -a;
int c = -a;

特点:

  • 操作数可以是左值也可以是右值
  • 表达式结果是右值

实现方式:

  • 成员函数方式
    #0 的表达式可以被编译器翻译成 0.operator#() 这样成员函数调用形式,该函数的返回值是右值。
  • 全局函数形式
    #0 的表达式可以被编译器翻译成 ::operator#(0) 这样成员函数调用形式,该函数的返回值是右值。

示例:

cpp
#include<iostream>
using namespace std;

class Int
{
public:
    Int(int data = 0) : m_data(data)
    {
    }
    const Int operator-()
    {
        Int res(-m_data);
        return res;
    }
    friend const Int operator~(const Int&);
    void print()
    {
        cout << "m_data = " << m_data << endl;
    }
private:
    int m_data;
};

const Int operator~(const Int& i)
{
    Int res(~i.m_data);
    return res;
}

int main()
{
    Int i1(10);
    Int i2 = -i1;
    Int i3 = ~i1;
    i1.print(); // 10
    i2.print(); // -10
    i3.print(); // -11 (按位取反)
    return 0;
}
1.2.2.2 自增/减单目操作符
  • 前++、--
    示例:
cpp
int a = 1;
int b = ++a; // b=2, a=2
int c = --a; // c=1, a=1

特点:

  • 操作数必须是左值
  • 表达式的结果也是左值,就是操作数自身

实现方式:

  • 成员函数方式
    #0 的表达式可以被编译器翻译成 0.operator#() 这样成员函数调用形式,该函数的返回值是左值。
  • 全局函数形式
    #0 的表达式可以被编译器翻译成 ::operator#(0) 这样成员函数调用形式,该函数的返回值是左值。

示例:

cpp
#include<iostream>
using namespace std;

class Integer
{
public:
    Integer(int i = 0) : m_i(i) {}
    void print(void) const
    {
        cout << m_i << endl;
    }
    // 前++: 成员函数形式
    Integer& operator++(void)
    {
        ++m_i;
        return *this;
    }
    // 前--: 全局函数形式
    friend Integer& operator--(Integer& i)
    {
        --i.m_i;
        return i;
    }
private:
    int m_i;
};

int main()
{
    Integer i(100);
    Integer j = ++i;
    i.print(); // 101
    j.print(); // 101

    j = ++(++i); // ok
    i.print(); // 103
    j.print(); // 103

    j = --i;
    i.print(); // 102
    j.print(); // 102

    j = --(--i); // ok
    i.print(); // 100
    j.print(); // 100

    return 0;
}
  • 后++、--
    示例:
cpp
int a = 1;
int b = a++; // b=1, a=2
int c = a--; // c=2, a=1

特点:

  • 操作数必须是左值
  • 表达式结果是右值,是操作数自增减前的副本

实现方式:

  • 成员函数方式
    #0 的表达式可以被编译器翻译成 0.operator#(哑元) 这样成员函数调用形式,该函数的返回值是右值。
  • 全局函数形式
    #0 的表达式可以被编译器翻译成 ::operator#(0, 哑元) 这样成员函数调用形式,该函数的返回值是右值。

在 C++ 中,后缀增量运算符(++)的语义是:先返回旧值,然后对操作数加 1。为了区分前缀和后缀版本的运算符,C++ 标准规定了一种特殊的函数签名来重载后缀增量运算符。

示例:

cpp
#include<iostream>
using namespace std;

class Integer
{
public:
    Integer(int i = 0) : m_i(i) {}
    void print(void) const
    {
        cout << m_i << endl;
    }
    // 后++成员函数形式
    const Integer operator++(int /*哑元*/)
    {
        Integer old = *this;
        ++m_i;
        return old;
    }
    // 后--全局函数形式
    friend const Integer operator--(Integer& i, int /*哑元*/)
    {
        Integer old = i;
        --i.m_i;
        return old;
    }
private:
    int m_i;
};

int main()
{
    Integer i(100);

    Integer j = i++; // i.operator++(0)
    i.print(); // 101
    j.print(); // 100

    j = i--; // i.operator--(0)
    i.print(); // 100
    j.print(); // 101

    return 0;
}
1.2.3 插入和提取运算符

示例:

cpp
int a = 1;
cin >> a;
cout << a;

功能:
实现自定义类型对象的直接输出或者输入。

实现方式:
只能用全局函数形式。

全局函数形式:
ostream 表示输出流、istream 表示输入流。

cpp
friend ostream &operator<<(ostream &os, const 类型名& 对象);
friend istream &operator>>(istream &is, 类型名& 对象);

示例:

cpp
#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(int r, int i) : m_r(r), m_i(i) {}
    friend ostream& operator<<(ostream& os, const Complex& c)
    {
        os << c.m_r << "+" << c.m_i << "i";
        return os;
    }
    friend istream& operator>>(istream& is, Complex& c)
    {
        is >> c.m_r >> c.m_i;
        return is;
    }
private:
    int m_r; // 实部
    int m_i; // 虚部
};

int main()
{
    Complex c1(1, 2);
    Complex c2(3, 4);

    cout << c1 << endl; // 1+2i
    cout << c1 << ',' << c2 << endl; // 1+2i,3+4i

    Complex c3(0, 0);
    cout << "请输入实部和虚部: ";
    cin >> c3; // operator>>
    cout << c3; // 输出输入的复数
    return 0;
}
1.2.4 下标操作符重载

示例:

cpp
int arr[5] = {1, 2, 3, 4, 5};
arr[0] = 3;

功能:
将该类中下标的元素取出。让一个对象像数组一样去使用。

特点:

  • 操作数既可以是左值也可以是右值
  • 非常对象返回左值,常对象返回右值

实现方式:
只能以成员函数方式。

示例:

cpp
#include<iostream>
using namespace std;

// 定义表示容器的类,里面存放多个int
class Array
{
public:
    Array(size_t size)
    {
        m_data = new int[size];
        for (size_t i = 0; i < size; i++)
        {
            m_data[i] = i + 1;
        }
        m_size = size;
    }

    ~Array(void)
    {
        delete[] m_data;
    }

    int& operator[](size_t i)
    {
        return m_data[i];
    }

    const int& operator[](size_t i) const
    {
        return m_data[i];
    }

private:
    int* m_data;
    size_t m_size;
};

int main()
{
    Array arr(10);
    arr[0] = 100; // arr.operator[](0)
    arr[1] = 200; // arr.operator[](1)
    cout << arr[0] << "," << arr[1] << endl; // 100,200

    const Array& rarr = arr;
    cout << rarr[0] << "," << rarr[1] << endl; // 100,200
    // rarr[0] = 123; // Error: 常量对象不能修改
    return 0;
}

1.2.5 运算符重载的限制

在 C++ 中,大部分的运算符都可以重载,程序员可以根据不同的需求来实现不同的功能。但是存在一些运算符是不能重载的。这些运算符不能被重载的原因是因为它们在语义上具有特殊的含义,不能被改变。

  • ::(作用域解析运算符):运算符用于访问全局命名空间或类的静态成员。它在语法上具有固定的含义和用途。它用于指定作用域,以解决命名冲突和访问作用域中的成员。如果允许重载双冒号运算符,可能会导致混淆和不一致的语义。
  • .->(成员访问运算符):运算符用于访问对象的成员。点运算符 . 用于直接访问对象的成员,箭头运算符 -> 用于通过指针访问对象的成员。这两个运算符不能被重载的原因是它们在语法上具有固定的含义和用途。
  • .*(成员指针访问运算符):运算符主要用于通过类成员指针来访问类的成员函数或成员变量。这个运算符在语法和语义上具有特殊的含义,编译器会自动处理相关的内存地址和偏移量计算。
    示例:
    cpp
    #include <iostream>
    
    class MyClass {
    public:
        int value;
    
        void printValue() {
            std::cout << "Value: " << value << std::endl;
        }
    };
    
    int main() {
        MyClass obj;
        obj.value = 42;
    
        void (MyClass::*funcPtr)() = &MyClass::printValue; // 声明一个成员函数指针
        (obj.*funcPtr)(); // 通过成员函数指针调用成员函数
    
        int MyClass::*dataPtr = &MyClass::value; // 声明一个成员变量指针
        std::cout << "Value: " << obj.*dataPtr << std::endl; // 通过成员变量指针访问成员变量
        return 0;
    }
  • ?:(三目运算符):运算符用于条件表达式。不能重载三元条件运算符 ?: 的原因是它在语法上具有特殊的含义和用途。它用于在两个表达式之间进行选择,并根据条件的真假来返回相应的值。
  • sizeof(大小运算符):运算符用于获取类型或对象的大小。由于 sizeof 运算符不是一个函数或操作符,而是由编译器特殊处理的运算符,因此它不能被用户自定义重载。
  • 类型转换运算符static_castdynamic_castconst_castreinterpret_cast):用于进行类型的转换。类型转换运算符在语法和语义上具有特殊的含义和用途。
  • newdelete(动态内存管理运算符):使用 newdelete 关键字可以在程序运行时动态地分配和释放内存。
  • *(解引用运算符):运算符可以重载,但是不建议。

2. 内联函数

内联函数是定义在类声明或实现体中的函数,其前面用 inline 关键字修饰。当编译器遇到内联函数调用时,它通常会将整个函数的代码插入到调用该函数的地方,而不是进行常规的函数调用。这样做的目的是为了消除函数调用的开销,从而提升程序的运行效率。

  • 如果类的成员函数在类中定义而不是把声明和定义分开,这种情况成员函数自动成为内联函数(inline)。如果类的成员函数手动声明为 inline,那么定义必须写在调用这个函数的文件中。
  • 使用 inline 关键字(要写在函数定义处才会生效)修饰的函数,表示这个函数是内联函数,编译器会尝试进行内联优化,减少函数调用的开销。
  • 使用场景:
    • 多次调用/小而简单的函数适合做内联优化
    • 调用次数极少或者大而复杂的函数不适合内联
    • 递归函数不能内联

内联只是一种建议而不是强制要求,一个函数能否内联优化,主要取决于编译器。有些函数不加 inline 修饰也会被编译器默认处理为内联优化,有些函数即使加了 inline 关键字也会被编译器忽略掉(慎用内联)。

内联函数是一种优化手段,通过减少函数调用的开销来提升程序性能。但需要注意的是,过度使用内联函数可能会导致代码膨胀,反而降低性能,因此需要谨慎使用。

知识如风,常伴吾身