面向对象学习笔记(避雷篇)
面向对象学习笔记(避雷篇)
时间还挺快的,当年的 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;
这样的句子不被视为 “定义” 行为,而是 “声明” 行为。因为 a
的 static
属性,导致它不属于任意一个对象,所以在开内存的时候,它不能占用任意一个对象的内存,也不能被构造函数初始化。
所以,需要在类外,再次单独给出一次 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 函数的实现一起带走。
有人会问这样是不是违反「头文件里只有声明没有定义」的规则?
我只能说,这样写是正确并且安全的,具体为什么,我也不知道。