OOP - C++笔记

错误:此声明没有存储类或类型说明符

#include <iostream>
using namespace std;
namespace ns1 {
    int inflag;
}
namespace ns2 {
    int inflag;
}
ns1::inflag = 2;  // 错误:此声明没有存储类或类型说明符
ns2::inflag = 3;  // 错误:此声明没有存储类或类型说明符

int main(int argc, char const *argv[]) {
    cout << ns1::inflag << endl;
    cout << ns2::inflag << endl;
    return 0;
}

如果将9、10行代码移入主函数内就不会报错。

这是因为上述代码的9、10行是赋值语句,而在Cpp中函数外只能定义全局变量或者对象 ,而不能执行语句及调用函数 。因此,移到函数内,那么赋值语句是允许的。

重载函数的要求

重载函数的参数个数或类型必须至少有其中之一不同。不允许重载的函数只有返回类型不同。

函数模板不是一个实实在在的函数,编译系统并不产生任何执行代码。当编译系统在程序中发现有与函数模板中相匹配的函数调用时,便生成一个重载函数,该重载函数的函数体与函数模板的函数体相同。

复制构造函数

需要复制构造函数的三种情况

  1. 新建立一个对象,需要另一个对象对其初始化

    Box box1(1);
    Box box2(box1);
  2. 对象作为形参

    void fun(Box b) {
        ...
    }
  3. 对象作为返回值

    Box fun() {
     ...
    }

静态成员变量

需要在类外初始化

class A {
    static int x, y;
}
int A::x = 0;
int A::y = 1;

静态成员函数

类内外定义均可

// 类内
static void number() { 
    cout << total << endl;
}
// 或者类外
void Student::number() {
    cout << total << endl;
}

类成员指针

指向非静态成员函数的指针

形式:数据类型名 (类名:: 指针变量名)(参数列表) = 类名::成员函数名;

int (Point::*getxP)() = Point::getX;
cout << (p.*getxP)() << endl;

指向静态成员(函数)的指针

对类的静态成员的访问是不依赖于对象的,因此可以用普通的指针来指向和访问静态成员。

// 静态成员变量
int *countp=&Point::count;

// 静态成员函数
void (*gc)()=Point::GetC;
// 调用
(*gc)();

构造函数调用顺序

先调用对象成员的构造函数,再调用本类的构造函数。析构函数的调用顺序刚好相反。

类模板

类模板外定义成员函数时,每一个函数前均加上:

template

运算符重载

运算符重载本质上是一种特殊的函数重载,运算符重载的函数参数就是该运算符涉及的操作数,因此运算符重载在参数个数上是有限制的(作为成员函数,参数最多有一个;作为友元函数,最多两个),这是它不同于普通的函数重载之处

自增运算符

++为前置运算符时,它的运算符重载函数的一般格式为:

operator ++( )

++为后置运算符时,它的运算符重载函数的一般格式为:

operator ++(int)

两种重载方式

当运算符重载为类的成员函数时,对于单目运算符,操作数一定是对象本身,对于双目运算符,左操作数一定是对象

当运算符重载为友元函数时,参数中同样必须有一个是用户自定义类型的对象,但不限定其必须是左操作数

基本类型与类类型的转换

基本类型到类类型

转换构造函数,如;

Complex(double r) {
    real = r;
    imag = 0;
}
// 有了上述函数,以下语句即合法
Complex c = 3.14;

类类型到基本类型

要将类对象转换为基本类型数据,需要一个特殊的成员函数——类型转换函数(实际就是类型转换运算符的重载,也正因为其实是对运算符的重载,这一函数只能是成员函数而不能是友元函数,与因为运算符重载的对象需要this指针)。

// 格式  (注意,没有返回值和参数!
operator〈返回基本类型名〉()
{
    ……
    return 〈基本类型值〉
}

例:

class Complex {
    double Real, Imag;

  public:
	...
    operator double(); //成员函数,定义类转换 Complex->double
};
Complex::operator double() { 
    return Real * Real + Imag * Imag; 
}

int main() {
    Complex c1(3.7, 4.5);
    double d;
    d = 2.5 + c1; // 隐式调用类型转换函数
    cout << d << endl; // 隐式调用类型转换函数
    return 0;
}

继承与派生

类的继承方式有public公有继承)、protected(保护继承)和private(私有继承)三种。默认情况下为私有继承

三种继承方式下派生类中基类成员的访问控制权限

公有继承私有继承保护继承
公有成员公有私有保护
私有成员派生类成员无法访问派生类成员无法访问派生类成员无法访问
保护成员保护私有保护

构造函数与析构函数

派生类调用基类的构造函数

派生类只能在构造函数的初始化列表中调用基类的构造函数,而不能在函数体中调用

Rectangle(float x, float y, float width, float height)
        :Point(x, y), w(width), h(height) {}  // √
Rectangle(float x, float y, float width, float height)
        :w(width), h(height) {
	Point(x, y);    // ×  
}

因为是先调用基类的构造函数再调用派生类的构造函数,而函数体就已经是在派生类的构造函数了,因此会先调用基类的默认构造函数,这里的初始化目的并没有达到,因为派生类构造函数体中的基类构造函数调用只是对一个临时对象进行了初始化(调试时发现this指针不同)。

调用顺序

构造函数调用顺序为:基类的构造函数→对象成员构造函数→派生类的构造函数。

析构函数调用顺序刚好相反。

多继承

处于同一层次的各基类构造函数的调用顺序取决于定义派生类时所指定的基类顺序,与派生类构造函数中所定义的成员初始化列表顺序无关。

如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

派生类新增加的同名成员会隐藏基类中的同名成员,因此

// 若C是A和B的派生类且三个类都有a成员变量
C c1;
c1.a = 3;    //这样是修改了C类的成员
// 若想访问基类的成员,需要加作用域运算符
c1.A::a = 3;
c1.B::a = 3;

虚基类

image-20201202170517362

这样的话C类会从A类和B类继承下来两份N类的成员,这时就无法区分了。

为了避免出现这样的二义性问题,可以将直接基类(如A、B)的共同基类(如N)设置为虚基类,这样共同基类(N)在内存中只有一个副本存在因此其构造函数也只会调用一次)。

虚基类的定义格式为:

class <派生类名>: virtual <继承方式><共同基类名>;

为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。

包含虚基类的构造函数调用顺序
  1. 先调用虚基类的构造函数,再调用非虚基类的构造函数。

  2. 若同一层次中包含多个虚基类,其调用顺序为定义时的顺序

  3. 若虚基类由非虚基类派生而来,则仍按先调用基类构造函数,再调用派生类构造函数的顺序。

多态

从系统实现的角度,多态性分为两类:

  1. 静态多态性:在程序编译时系统就能决定调用的是哪个函数。又称为编译时的多态性。静态多态性是通过函数的重载实现的(包括运算符重载)。

  2. 动态多态性:在程序运行过程中才动态地确定操作所针对的对象。又称为运行时的多态性。动态多态性是通过虚函数实现的

虚函数

虚函数的使用:

  1. 类之间应满足类型兼容原则
  2. 同名声明虚函数
  3. 通过指针、引用来访问虚函数

注意,类型兼容原则是以下三种情况:

  1. 派生类的对象可以赋值给基类的对象。
  2. 派生类的对象可以初始化基类的引用。
  3. 派生类的对象的地址可以赋值给基类的指针变量。

而访问虚函数只能是后两者。

若要访问派生类中相同名字的函数,必须将基类中的同名函数定义为虚函数,这样,就可以动态地根据基类的引用或指针调用不同类中的函数。

一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中该函数都保持虚函数特性。因此,在派生类中重新定义该函数时,可以省略关键字virtual。

构造函数不能是虚函数。 虚函数作为运行时的多态性的基础,主要是针对对象的,而构造函数是在对象产生之前运行的。所以,将构造函数声明为虚函数是没有意义的。

析构函数可以是虚函数。比如释放内存时,由于实施多态性时是通过将基类的指针指向派生类的对象来完成的,如果删除该指针,就会调用该指针指向的派生类的析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象才被完全释放。

纯虚函数

virtual <函数值类型> <函数名>(<参数表>) = 0

纯虚函数不定义实现方法,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态的接口。

抽象类

包含一个或多个纯虚函数的类称为抽象类。