OOP 学习笔记(2)——封装与接口

OOP 学习笔记(2)——封装与接口

Contents

封装与接口

函数重载

同一名称的函数,有两个以上不同的函数实现,被称为函数重载

比如:

int ADD(int a,int b)
{
    return a+b;
}
int ADD(int a,int b,int c)
{
    return a+b+c;
}

编译器将根据函数调用语句的实际参数决定调用哪个函数。

多个同名函数之间必须保证可以通过参数列表区分,返回值、参数名称不能作为区分标识。

函数参数的缺省值

也就是讲过的函数参数初始值。

同样保证需要可区分。

基础知识

auto

由编译器根据上下文自动确定变量的类型,如:

auto i = 3;     //i是int型变量
auto f = 4.0f;  //f是float型变量
auto a('c');    //a是char型变量
auto b = a;     //b是char型变量
auto *x = new auto(3);  //x是int*

追踪返回类型的函数

可以将函数返回类型的声明信息放到函数参数列表的后面进行声明。如:

  • 普通函数声明形式
    int func(char *ptr, int val);
    
  • 追踪返回类型的函数声明形式
    auto func(char *ptr, int val) -> int;
    

追踪返回类型在原本函数返回值的位置使用 auto 关键字。

decltype

  • decltype 可以对变量或表达式结果的类型进行推导。
  • 重用匿名类型。
  • 泛型编程中结合 auto,用于追踪函数的返回值类型。
struct
{
    //...
}a, c[10];
int main()
{
    decltype(a) b;
    decltype(c) d;
    //定义了一个上面匿名的结构体
}

结合 autodecltype

  • 可以推导返回类型:
    auto func(int x, int y) -> decltype(x + y)
    {
      return x + y;
    }
    
  • C++14 中不再需要显式指定返回类型:
    auto func(int x, int y)
    {
      return x + y;
    }
    

内存的动态申请和释放

也就是 new/delete 运算符。

直接举例就是:

int *ptr = new int;
int *array = new int[10];
delete ptr;
delete [] array;

跟前面的 malloc()free 很像,不做更多说明。

NULL、$0$、nullptr

在 C++ 中,NULL 被定义为 $0$。

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

在 C++11 之前,可以使用 NULL 或者 $0$ 表示空指针。

然而这样会产生一些问题:

void f(int x, double *y)
{
    //...
}
f(2, NULL); // 程序可能报错
f(2, static_cast<double *>(0)); // 程序正常运行

我们可能会忽略 NULL 同时是一个 int 型常量的事实。

nullptr 表示严格意义上的空指针。

此时上面程序就不会报错。

基于范围的 for 循环

形式为:

for (type key : range)
   //...

其中 key 是用于迭代的变量,range 表示被迭代的范围。

如:

int arr[] = {2, 3, 5, 7, 11};
for (int e : arr)
    cout << e << endl;

用户定义类型——类 class

class——用户自定义的类型

  • 包含函数、数据的加强版“结构体”(C 语言中的)。
  • 其中包含函数和数据称为成员函数成员变量

  • 成员函数必须在类内声明,但定义可以在类内或者类外。

    • 利用这点,往往会在头文件中声明类,并在实现文件中定义成员函数。
    • 一般将不同的类保存为不同的头文件和实现文件。
    • 当然实际上也可以在类内直接定义成员函数,但复杂成员函数一般还是声明、定义分离的 。
    class Matrix
    {
    public:
      void fill(char dir);
    private:
      int data[6][6];
    };
    void Matrix::fill(char dir)
    {
      data[0][0] = 1;
      //...
    }
    

类成员的访问权限

类的成员变量和函数可以根据需要设置不同的访问权限:

  • public:可以在类外用 . 操作符访问。
  • private
    • 默认权限;(没有注明访问权限情况下)
    • 不允许在类外用 . 操作符访问。
  • protected:暂时不介绍。

具体实例可以自行尝试。

this 指针

所有成员函数的参数中,隐含一个指向当前对象的指针变量,名称为 this

如上面的 fill() 函数中,就可以将 data 改为 this->data 而不改变效果。

类的运算符重载

用户自定义类,没有对重用运算符进行定义,比如对于两个类对象,难以使用 a+b

所以,C++ 支持了对以下运算符的重载:

  • +-*/[]()<<>>++--

对于加减乘除我们就以 + 代替,其定义一般为:

return_type operator+(parameters)
{
    //...
}

比如上面的 Matrix 类可以定义矩阵加法:

Matrix operator+(const Matrix &A, const Matrix &B)
{
    Matrix ret;
    for (int i = 0; i < 6; i++)
        for (int j = 0; j < 6; j++)
            ret.data[i][j] = A.data[i][j] + B.data[i][j];
    return ret;
}

对象输入输出——流运算符重载

用户自定义的类,虽然可以像内置类型那样定义变量(对象),但想要使用流运算符输入、输出对象,则还需要为其定义流运算符重载。

比如想要实现这样的功能:

Matrix A;
cin >> A;
//...
cout << A;

流运算符重载函数的声明

istream& operator>>(istream& in, Type& dst);
ostream& operator<<(ostream& out, const Type& src);

一种可能的实现为:

#include <iostream>
#include <fstream>
using namespace std;

class Matrix
{
    int data[6][6];
public:
    friend istream& operator>>(istream& in, Matrix& dst);
    friend ostream& operator<<(ostream& out, const Matrix& src);
};
istream& operator>>(istream& in, Matrix& dst)
{
    for (int i = 0; i < 6; i++)
        for (int j = 0; j < 6; j++)
            in >> dst.data[i][j];
    return in;
}
ostream& operator<<(ostream& out, const Matrix& src)
{
    for (int i = 0; i < 6; i++)
        for (int j = 0; j < 6; j++)
            out << dst.data[i][j] << (j == 5 ? '\n' : ' ');
    return out;
}

函数运算符 () 重载

这种重载让对象看上去像是一个函数名:

return_type operator()(parameters)
{
    //...
}

使用方法:

ClassName Obj;
Obj(real_parameters);
// -> Obj.operator()(real_parameters);

此时 Obj 对象看上去就像是一个函数,故也称为函数对象,似乎也被称为伪函数

数组下标运算符 [] 重载

与前面的基本相同,但是注意只能传入一个参数。

当然这个参数的类型倒是基本没有限制,也就是可以以此构造广义的数组。

注意,如果返回值是引用,则可以作为左值,否则不行。

一个实例:

#include <iostream>
#include <string>
using namespace std;

char week_name[7][4] = {"mon", "tu", "wed", "thu", "fri", "sat", "sun"};

class WeekTemp
{
    int temp[7];
public:
    int& operator[] (const char* name) // 字符串作下标
    {
        for (int i = 0; i < 7; i++)
        {
            if (strcmp(week_name[i], name) == 0) 
                return temp[i];
        }
    }
};

int main() 
{
WeekTemp beijing;
    beijing["mon"] = -3;    
    beijing["tu"] = -1;
    cout    << "Monday Temperature: " 
            << beijing["mon"] << endl;

    return 0;
}

输出结果:

Monday Temperature: -3

前缀与后缀的 ++--

前缀运算符重载声明:

  • ClassName operator++();
  • ClassName operator--();

后缀运算符重载声明:

  • ClassName operator++(int dummy);
  • ClassName operator--(int dummy);

通过在函数体中没有使用的哑元参数 dummy 来区分前缀与后缀的同名重载。

一般哑元只定义但没有名称,比如:

#include <iostream>
using namespace std;

class Test
{
public:
    Test operator++()
    {
        cout << "operator++()" << endl;
    }
    Test operator++(int)
    {
        cout << "operator++(int)" << endl;
    }
};

int main()
{
    Test test;
    test++;
    ++test;
    return 0;
}

友元

类内进行友元的声明

被声明为友元的函数或类,具有对出现友元声明的类privateprotected 成员的访问权限,也就是可以访问该类的一切成员。

友元修饰的函数或类,不受 private 的影响。

class Test
{
    int data;
    friend void f(Test &test);
};

void f(Test &test)
{
    cout << test.data << endl;
}

上面的流运算符重载就是需要访问私有成员 data 数组才会加上友元声明。

也可以声明别的类的成员函数,为当前类的友元(包括构造函数、析构函数,后面内容):

class A
{
    void f(int);
    A(int x)
    {
        //...
    }
    ~A()
    {
        //...
    }
};
class B
{
    int x;
    friend void A::f(int);
    friend A::A(int x),A::~A();
};

同时也可对 classstructunion 进行友元声明。

对其他类型的友元声明只会被忽略。

class A {};
class B
{
    friend class C; // 友元类前置声明(详细类型指定符)
    friend A; // 友元类声明(简单类型指定符)(c++11 起)
};
class C {};

而一个函数也可以是多个类的友元函数。

其他注意事项还有

  • 友元不传递。
  • 友元不继承。
  • 友元声明不能用于定义新的 class

内联函数

用途

使用内联函数,编译器自动产生等价的表达式。

inline int max(int a, int b)
{
    return a > b ? a : b;
}
cout << max(a, b) << endl;

等价于:

cout << (a > b ? a : b) << endl;

和宏定义的区别

首先,宏定义是直接拷贝,可能与函数调用有不同的结果,容易出错,很多缺陷不可避免

其次,内联函数在 Debug 版本中,没有真正内联,仍然可以调试,在 Release 版本,才实现了真正的内联。

最后,宏定义的函数无法操作私有数据成员

注意事项

  • 避免对大段代码使用内联修饰符。
  • 避免对包含复杂结构的函数使用内联定义。
  • 内联修饰用于函数定义而不是函数声明。
  • 定义在类声明中的函数默认为内联函数。
  • 一般构造函数、析构函数都被定义为内联函数。
  • 在头文件加入或修改 inline 函数时,调用此头文件的源文件均需要重新编译。
  • 内联修饰符更像是建议而非命令,实际上编译器往往会有判断性、智能地选择是否内联。

 

点赞 0

No Comments

Add your comment