面向对象学习笔记(避雷篇)

面向对象学习笔记(避雷篇)

时间还挺快的,当年的 OIer 已经上大学啦!在大学的 C++ 学习中,要求我们掌握两种编程技巧,即面向对象和面向过程。

因为高中时期不管是 CSP 还是 NOIp 的比赛,大家用的基本上都是面向过程的设计思路,因此我开学后就着重开始学习面向对象的方法。

这里对面向对象的概念不做细致的讨论了。总之,复杂的也用不到,现在的水平只能做到 “封装” 这一项。

下面来整理一下最近写代码时候遇到的困难和解决办法。

在类外调用类内的静态函数

大多数时候,我们要指定特定的某一个对象去调用 public 中的函数,但是也有一些情况,不可避免的要直接调用类内 public 中的函数,这时候可以使用静态函数的方法。

class Example
{
  static void test()
  {
    //do something ...
  }
};

//在其他函数中

Example::test();

原理:
静态关键词 static 表示这是类内公用的一个函数/变量(静态函数/变量),它不属于类内的任意一个对象。因此,可以跳过指定对象的环节直接调用。

在类外调用类内的静态变量

类似上面,我们有一个公共的变量 a 。不管定义了多少个对象,它们都公用这一个变量 a ,相应的,变量 a 也只占对应变量类型的空间大小,不会随对象的增多而占用更多的空间。

在类内的函数中,可以直接调用 a

在类外的函数中,可以使用 classname::a 来调用变量 a

class Example
{
  static int a;
  void test1()
  {
    std::cout << a;
  }
};

void test2()
{
  std::cout << Example::a;
}

int Example::a;
// 这一句很重要,下面讲为什么

需要注意的是,在类内写出 static int a; 这样的句子不被视为 “定义” 行为,而是 “声明” 行为。因为 astatic 属性,导致它不属于任意一个对象,所以在开内存的时候,它不能占用任意一个对象的内存,也不能被构造函数初始化。

所以,需要在类外,再次单独给出一次 a 的 “定义”,分配给它一个内存。如果需要的话,也要单独的初始化它。这样才能正常地调用静态变量 a

如果不在类外重新定义 a 的话,编译器会报错 "undefined symbol" 。

在类外强行访问 private 中的元素

尽管这有违面向对象的初衷——保护对象中的元素不能直接从外部访问,但是有时候我们不得不这样做,这就用到了友元关键词 friend

关键词 friend 可以授权其他的类或者函数直接访问该类中的 private 或者protected 中的成员。

class Example
{
  public:
    friend void test1();
  private:
    int a;
}e;

void test1()
{
  std::cout << e.a;
}
//test1() 可以运行,因为 friend 关键词授予了它访问 private 元素的权限

void test2()
{
  std::cout << e.a;
}
//test2() 不能运行,报错显示无权访问 private 中的元素

构造函数需要传参

如果使用构造函数初始化的话,那么大概率需要传参,但是参数又大多都是用户输入的,这就会导致无法把对象定义为全局变量(因为不知道参数,定义的时候会因为构造函数中没有参数而报错)。

** upd 2023.11.8 :博主在后续的学习中意识到可以通过重载一个不带参数的 default constructor 来解决这件事情。但是这不失为一种解决方法,故保留这部分。**

一种显而易见的解决方法是不要用构造函数初始化,自己另写一个带 static 关键词类型的初始化函数,然后在主函数中调用。

如果自带反骨,一定要用带参数的构造函数初始化,并且还想开全局变量的话,也有办法:使用 std::vector 。因为 vector 容器的初始长度为 0 ,不占用任何内存,所以声明的时候不会调用构造函数,避免了报错。

在主函数中得到数据之后,可以再把数据打包起来然后用构造函数加 pushback() 函数塞到类里面。

class Example
{
  public:
    Example(const int A[])
    {
      for(int i = 0 ; i < 3 ; i ++)
        a[i] = A[i];
    }
  private:
    int a[3];
};

std::vector < Example > V;

int main()
{
  int a[3];
  for(int i = 0 ; i < 3 ; i ++)
    std::cin >> a[i];
  V.push_back( Example(a) );
  return 0;
}

构造函数的初始化列表

用初始化列表来写构造函数会让代码更整洁!

class Example
{
  public:
    Example(int A, int B) : a(A), b(B){}
  /*
    等价于
    Example(int A, int B)
    {
      a = A;
      b = B;
    }
  */
  private:
    int a, b;
};

另外有个顺序问题,详情可以看这篇 blog

初始化列表在类的组合和继承中也有广泛的应用。

inline small functions(内联函数)

  • 首先我们需要了解 inline 的工作原理:把内联函数的代码复制到调用它的地方,编译时不在可执行文件中产生该函数

问题的产生

假设我们现在有三个文件,分别是 a.cpp, main.cpp, a.h

在 a.h 头文件中:

#ifndef A_H_
#define A_H_

inline void f();

#endif

在 a.cpp 文件中:

#include <iostream>
#include "a.h"

using namespace std;

inline void f()
{
  cout << "Hello, World!" << endl;
}

在 main.cpp 文件中:

#include "a.h"

int main()
{
  f();
  return 0;
}

然后我们编译这两个文件,却报错了,这是为什么呢?

main.cpp 先预编译了头文件 a.h,也就是把 a.h 的代码复制进来。(如果不懂头文件和分步编译规则的可以看这里

这时候 main.cpp 变成了这样:

#ifndef A_H_
#define A_H_

inline void f();

#endif

int main()
{
  f();
  return 0;
}

a.h 告诉编译器这是一个内联函数,需要把 f 函数的代码直接复制到调用它的地方。

但是 a.h 里只有 f 的声明,并没有它的定义(也就是没有具体的代码实现 f)。

编译器这时候蒙了,因为它一次只能编译一份代码,它不知道 f 的具体实现在 a.cpp 里。

编译器问头文件到底有没有 f 这个函数,如果有,具体实现是什么?

头文件:编译器,我有一个内联函数,你给我把他复制到要调用它的地方。

编译器:好的,请把要复制的代码给我,我来复制。

头文件:我不知道它具体代码是什么...

于是乎编译器只能把这个难题交给程序员,它发了一个警告,说 f 函数内联失败,然后放弃内联继续编译。

虽然编译器放弃了在 main.cpp 中的内联,但它还是需要一个 f 函数的具体实现,编译器先把需要 f 的地方留了个坑,继续编译下一份代码。

当编译器去编译 a.cpp 的时候,它找到了一个 inline 的 f,但是 inline 关键字使得它被尝试复制到调用它的地方之后,原函数体(实现)不被保留。

这就导致 a.cpp 里也没有 f 函数的函数体了,链接器在链接两份代码的时候,就掉到编译器在 main.cpp 里留的那个坑里。

这个坑需要一个 f 函数的函数体来填上,但是链接器找不到这样一个函数体,于是它就只能报错了。

避免问题

  • 我们只要把所有的 inline 内联函数的实现统统写在头文件里就可以了,这样引用头文件的时候就会把 inline 函数的实现一起带走。

有人会问这样是不是违反「头文件里只有声明没有定义」的规则?

我只能说,这样写是正确并且安全的,具体为什么,我也不知道。

posted @ 2023-09-24 15:53  ZTer  阅读(19)  评论(0编辑  收藏  举报