2. Cpp面向对象编程(OOP)入门

Tip

C++代码块没有包含头文件时,默认是:

#include <iostream>
using namespace std;

1 类和继承

1.1 类的概念

1.1.1 结构体(Struct, C)

. 访问结构体的成员.

#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 借助结构体进行数据抽象
struct Student{
    int ID;
    int classNum;
    string name;
};

int main() {
    vector<Student> students;  // 声明一个向量students, 元素类型是结构体Student
    Student stu1;  // 声明结构体对象stu1
    stu1.ID = 5;  // 用点号访问结构体中的成员
    stu1.classNum = 1;
    stu1.name = "大宝";
    students.push_back(stu1);  // 将stu1添加到students数组
    Student stu2;  // 类似上面
    stu2.ID = 6;
    stu2.classNum = 1;
    stu2.name = "小宝";
    students.push_back(stu2);
    for ( int i = 0; i < 2; i++ ) {
        cout << "该学生的学号是:" << students[i].ID << endl;
        cout << "该学生的班级是:" << students[i].classNum << endl;
        cout << "该学生的名字是:" << students[i].name << endl;
        cout << endl;
    }
    return 0;
}
-----------------------------------------------------------
输出:
该学生的学号是:5
该学生的班级是:1
该学生的名字是:大宝

该学生的学号是:6
该学生的班级是:1
该学生的名字是:小宝

1.1.2 封装

封装: 隐藏细节, 将一个复杂的装置封装在一个黑匣子, 只提供一些用户接口. 如, 在使用函数时, 只需要关注参数类型 && 返回值 && 函数的作用, 而不需要关系函数体内具体的代码.

类(Class)是 C++在 C 的结构体之上进行扩展的新类型, 可以同 Struct 一般实现数据抽象, 也可以使用访问控制符有效地体现封装. 比如, 上面的 vector.push_back(...);, 就是 vector 类的使用, 我们可以使用它的 push_back()pop_back() 等函数, 而不必关心 vector 是如何实现的.

1.1.3 继承和多态

在设计 Class 的时候, 为了避免重复的数据和函数, 就会使用继承来让类形成一个树形的层级结构.

继承也是为了实现多态而存在的. 多态: 不同的类具有相似的行为. 在 C++中可以使用虚函数声明同名的函数, 并在不同的类中进行不同的实现.

1.2 类的定义

面向对象的核心是类, 它是 C++ 在 C 语言原有结构体的基础之上扩展出来的概念, 不仅增加了附属于类的成员函数, 也增加了继承和虚函数等面向对象编程所需要的重要功能.

从 Class 创建出来的具体变量(实例)则叫作对象(Object). 每个对象占有着独立的内存空间, 而类只是一个描述对象的抽象概念(模板). 对象和类可以理解为是糕点和做糕点的模具, 糕点的形状都相同, 但可能是不同材料做的. 当然, 有时我们也会将这两个术语混用. 先来看一个简单的类定义.

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

// 类的定义
class Champion{ 
	public:
		Champion(int id, string nm, int hp, int mn, int dmg) {  // 此为构造函数, 见后文
			ID = id;
			name = nm;
			HP = hp;
			mana = mn;
			damage = dmg;
		}
		void attack(Champion &chmp) {  // 攻击. 当当前英雄(调用`attack`函数的英雄)发起攻击时, 会调用被攻击英雄(`chmp`)的`takeDamage`函数, 并传入当前英雄的`damage`(伤害值). 这里的`this->damage`指的是 “当前攻击者” 的伤害属性(`this`指针指向当前调用`attack`函数的英雄对象).
			chmp.takeDamage(this->damage); // 此处this指针, 表示当前攻击者的damage而不是被攻击者的damage, 实测此处this指针并不是必须的
		}
		void takeDamage(int incomingDmg) {  // 掉血. 当英雄受到伤害时, 将自身的`HP`(血量)减去受到的伤害值(`incomingDmg`), 直接修改自身的血量属性.
			HP -= incomingDmg;
		}
	private:
		int ID;
		string name;
		int HP; //血量
		int mana; //魔法值
		int damage; //伤害值
};

int main() {
	return 0;
}

1.2.1 成员变量

类中可以定义各种成员变量, 可以是基本数据类型或其他类的实例, 其初始值需要在构造函数中指定.

一个类的内部可再定义一个类, 不过该类的作用域将仅限于外层类中. 若是公有访问(public)级别, 则可使用作用域操作符 :: 在外部访问. 私有成员(private)一般仅限于类内部定义的成员函数的访问(封装).

1.2.2 Member Function (成员函数)

调用某个类实例的成员函数时, 使用 myObj.doSomething() 语法.

成员函数可以在类之内/外部定义. 若要在外部定义, 需要首先在类中声明函数, 然后在类外定义的函数名前加上类名和作用域符 ::.

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

// 成员函数定义与声明的分离
class Champion{
	public:
		Champion(int id, string nm, int hp, int mn, int dmg);  // 类内部提前声明
		void attack(Champion &);  // 类似的提前声明, 函数声明不需要像上一个代码块指定形参名`chmp`
		void takeDamage(int incomingDmg);  // 类似的提前声明
	private:  // 私有成员
		int ID;
		string name;
		int HP; //血量
		int mana; //魔法值
		int damage; //伤害值
	};
	
Champion::Champion(int id, string nm, int hp, int mn, int dmg) {   // 使用类名+作用域符 `::`在类之外定义成员函数(此处是构造函数)
	ID = id;
	name = nm;
	HP = hp;
	mana = mn;
	damage = dmg;
}
void Champion::attack(Champion &chmp) {  // 类似的定义方式
	chmp.takeDamage(this->damage);
}
void Champion::takeDamage(int incomingDmg) {  // 类似的定义方式
	HP -= incomingDmg;
}

int main() {
	return 0;
}

常量成员函数

在参数列表(函数声明或定义时)后加上 const 关键字(如 Champion(int id, string nm, int hp, int mn, int dmg) const; ), 可以创建常量成员函数, 在该函数中不可修改本对象的成员变量.

1.2.3 构造函数

在上述代码中, 有一个和类同名的特殊成员函数 Champion(), 称为 Constructor (构造函数). 其主要功能是初始化成员变量, 往往在类实例化(创建对象)时自动调用.

构造函数支持重载. 没有参数的构造函数被称为默认构造函数(Default Constructor). 没有自定义构造函数时, 系统会提供一个预置的默认构造函数, 将所有成员变量都初始化为默认值(与具体数据类型有关).

1.2.4 对象创建和使用

使用成员访问运算符 . 访问类实例的成员(函数和变量).

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

class Champion{
	public:
		Champion(int id, string nm, int hp, int mn, int dmg) {
			ID = id;
			name = nm;
			HP = hp;
			mana = mn;
			damage = dmg;
		}
		void attack(Champion &chmp) {
			chmp.takeDamage(this->damage);
		}
		void takeDamage(int incomingDmg) {
			HP -= incomingDmg;
		}
		int getHP() {
			return HP;
		}
	private:  // 私有成员不能在类实例访问, 如galen.ID不可行. 只能在类定义中被成员函数等访问
		int ID;
		string name;
		int HP; //血量
		int mana; //魔法值
		int damage; //伤害值
};

int main() {
	Champion galen(1, "Galen", 800, 100, 10);  // 定义对向时, 类名Champion被当作类型名使用, 后面括号是构造函数的参数. 若不给出参数, 将会调用自定义或系统预设的默认构造函数
	Champion ash(2, "Ash", 700, 150, 7);
	cout << "Ash的初始血量:" << ash.getHP() << endl;
	galen.attack(ash);  // 成员访问运算符 "."
	cout << "Ash受到Galen攻击后的血量:" << ash.getHP() << endl;
	return 0;
}
---------------------------------------------------------------------------
输出:
Ash的初始血量:700
Ash受到Galen攻击后的血量:690

也可以使用类的指针访问成员(函数或变量):

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

class Champion{
	public:
		Champion(int id, string nm, int hp, int mn, int dmg) {
			ID = id;
			name = nm;
			HP = hp;
			mana = mn;
			damage = dmg;
		}
		void attack(Champion &chmp) {
			chmp.takeDamage(this->damage);
		}
		void takeDamage(int incomingDmg) {
			HP -= incomingDmg;
		}
		int getHP() {
			return HP;
		}
	private:
		int ID;
		string name;
		int HP; //血量
		int mana; //魔法值
		int damage; //伤害值
};

int main() {
	Champion galen(1, "Galen", 800, 100, 10);
	Champion ash(2, "Ash", 700, 150, 7);
	cout << "Ash的初始血量:" << ash.getHP() << endl;
	Champion *chmpPtr = &galen; // 指向Champion的指针
	(*chmpPtr).attack(ash);  // 可以解引用再使用点符号
	chmpPtr->attack(ash);  // 也可以使用指针专用的成员访问符 "->", 两者等价.
	cout << "Ash受到Galen攻击后的血量:" << ash.getHP() << endl;
	return 0;
}
----------------------------------------------------------------
输出:
Ash的初始血量:700
Ash受到Galen攻击后的血量:680

也可以直接在类定义之后紧跟对象声明:

#include <iostream>
using namespace std;

class MyClass{
	public:
		MyClass() {
			a = 1;
		}
		int getA() {
			return a;
		}
	private:
		int a;
}myclass;  // 在类MyClass定义之后紧跟对象myclass声明

int main() {
	cout << "a的值是:" << myclass.getA() << endl;
	return 0;
}
-----------------------------------------------------
输出:
a的值是:1

1.2.5 this 指针

在成员函数访问成员时, 有一个隐含的指针变量 this 可用, 它的类型是指向当前实例的指针, 即指向正在调用该成员函数的对象.

#include <iostream>
using namespace std;

// this指针的显式使用
class MyClass{
	public:
		MyClass(int a, int b) {
			this->a = a;  // this指针就是当前类实例的地址/指针, 相当于python的`self`
			this->b = b;
		}
		int getA() {
			return this->a;  // 等价于 `return a;`
		}
	private:
		int a;
		int b;
};
int main() {
	MyClass myclass(2, 3);
	cout << "a的值是:" << myclass.getA() << endl;
	return 0;
}
-----------------------
输出:
a的值是:2

this 指针也可以作为成员函数的返回值, 用于获取对象的地址或副本:

#include <iostream>
using namespace std;

class MyClass{
	public:
		MyClass(int a, int b) {
			this->a = a;
			this->b = b;
		}
		MyClass *getAddr() {
			return this; // 获取地址, 返回 MyClass*
		}
		MyClass getCopy() {
			return *this; // 获取副本, 返回 MyClass
		}
		int getA() { return a; }
	private:
		int a;
		int b;
};
int main() {
	MyClass myclass(2, 3);
	MyClass *ptr = myclass.getAddr(); // 注意其用法
	cout << "a的值是:" << ptr->getA() << endl;
	MyClass copy = myclass.getCopy();
	cout << "a的值是:" << copy.getA() << endl;
	return 0;
}
-----------------------------------------------------------------------------
输出:
a的值是:2
a的值是:2

1.2.6 类和结构体的区别

结构体的默认访问控制符是 public, 而类是 private:

#include <iostream>
using namespace std;

// struct和class的默认访问控制符
class MyClass{
	// 加上public程序可以运行, 否则默认为private成员
//public: 
	MyClass(int a, int b) {
		this->a = a;
		this->b = b;
	}
	int a;
	int b;
};
struct MyStruct {
	// struct默认是public
	MyStruct(int a, int b) {
		this->a = a;
		this->b = b;
	}
	int a;
	int b;
};
int main() {
	MyClass myclass(2, 3); // error: ‘MyClass::MyClass(int, int)’ is private within this context, 私有成员函数无法访问
	MyStruct mystruct(2, 3);
	cout << "a的值是:" << myclass.a << endl; // error: ‘int MyClass::a’ is private within this context, 私有成员变量无法访问
	cout << "a的值是:" << mystruct.a << endl;
	return 0;
}

C 中的结构体没有构造函数和成员函数这些面向对象的元素, 而 C++ 补全了这一点. C++当中的 struct 也可以使用关键字 publicprivate, 因此两者的区别仅有上述一点: 默认访问控制符不同.

1.3 Constructor (构造函数)

1.3.1 默认构造函数

一般而言, 构造函数都会接受参数来初始化类的成员, 而默认构造函数是无参数的. 在创建对象时, 若对象名后面不加括号, 系统就会自动调用默认构造函数:

#include <iostream>
using namespace std;

class Time{
	public: 
		Time() {  // 默认构造函数不接受参数
			hour = 0;
			minute = 0;
			second = 0;
		}
		int hour;
		int minute;
		int second;
};
int main() {
	Time time;  // 创建对象时, 若对象名后面不加括号, 系统就会自动调用默认构造函数.
	cout << "时间是:" << time.hour << "时" << time.minute << "分" << time.second << "秒" << endl;
	return 0;
}
-----------------------------------------------
输出:
时间是:0时0分0秒

如果构造函数完全不存在, 系统就会自动生成一个默认构造函数, 它遵循基本数据类型的默认初始化规则.

1.3.2 重载构造函数

#include <iostream>
using namespace std;

class Area{
	public: 
		Area(int a, int b) {
			area = a * b;
		}
		Area(int a) {  // 重载构造函数
			area = a * a;
		}
		int getArea() { return area; }
	private:
		int area;
};
int main() {
	int a = 3;
	int b = 4;
	int c = 5;
	Area area1(a, b);
	Area area2(c);
	cout << "边长为" << a << "和" << b << "的长方形面积为:" << area1.getArea() << endl;
	cout << "边长为" << c << "的正方形面积为:" << area2.getArea() << endl;
	return 0;
}
------------------------------------------------------
输出:
边长为3和4的长方形面积为:12
边长为5的正方形面积为:25

1.3.3 初始化列表

除了使用赋值对类成员进行初始化, 也可以使用初始化列表来完成这一操作:

#include <iostream>
using namespace std;

class Time{
	public: 
	    Time(int hr, int min, int sec) : hour(hr), minute(min), second(sec) {} // 初始化列表以一个":"开始, 位于参数列表和函数体之间, 等同于赋值 hour=hr, minute=min, second=sec.
	    int getHour() { return hour; }
	    int getMinute() { return minute; }
	    int getSecond() { return second; }
	private:
	    int hour;
	    int minute;
	    int second;
};
int main() {
    Time time(12, 24, 36);
    cout << "时间是:" << time.getHour() << "时" 
                       << time.getMinute() << "分" 
                       << time.getSecond() << "秒" << endl;
    return 0;
}
--------------------------------------------------------
输出:
时间是:12时24分36秒

实际上, 基本数据类型也可以用类似的语法进行初始化:

int main() {
	int num(5);
	float fnum(3.4);
	char ch('h');
	bool bval(false);
	return 0;
}

初始化列表和赋值两种方式的区别可见下两个代码:

#include <iostream>
using namespace std;

// 初始化列表的调用顺序
// Author: 零壹快学
class B{
	public: 
		B() {
			cout << "B的构造函数被调用!" << endl;
			num = 0;
		}
	private:
		int num;
};

class A{
	public: 
		A(B bb) {
			cout << "A的构造函数被调用!" << endl;
			num = 0;
			b = bb;
		}
	private:
		B b;
		int num;
};

int main() {
	B b;
	cout << "创建A的对象之前!" << endl;
	A a(b);
	return 0;
}
-----------------------------------------------
输出:
B的构造函数被调用!
创建A的对象之前!
B的构造函数被调用!
A的构造函数被调用!

A 的成员 b 会先调用 B 的默认构造函数(因此出现输出的第三行), 再在 A 的构造函数体内通过 b=bb 赋值(一次赋值操作).

```cpp
#include <iostream>
using namespace std;

class B {
public:
    B(int x) {  // 给B添加带参数的构造函数, 方便观察
        cout << "B的带参构造被调用! x=" << x << endl;
    }
    B() {  // 保留默认构造
        cout << "B的默认构造被调用!" << endl;
    }
};

class A {
public:
    // 使用初始化列表, 故意打乱顺序: 先初始化num,再初始化b. 依然会先构造`b`, 再初始化`num`(因为`b`声明在前). 注: `int`是基本类型, 初始化顺序不影响结果, 但对于对象类型, 这个顺序至关重要
    A(B bb) : num(10), b(bb) {  // 初始化列表:num在前,b在后
        cout << "A的构造函数体执行!" << endl;
    }
private:
    // 成员声明顺序: 先声明b, 再声明num
    B b;
    int num;
};

int main() {
    B b(20);  // 创建B对象, 调用B的带参构造
    cout << "创建A对象前..." << endl;
    A a(b);   // 创建A对象, 传入参数b
    return 0;
}
-----------------------------------------------------
输出:
B的带参构造被调用! x=20
创建A对象前...
A的构造函数体执行!

A 的成员 b 的初始化会直接调用 B 的拷贝构造函数(编译器自动生成), 用 bb 直接初始化 b. 这个过程只需要一次拷贝构造, 省去了默认构造 + 赋值(以及 B 的默认构造函数中其他可能的语句)的额外开销, 因此更高效.

必须使用初始化列表的情况包括: 没有默认构造函数的成员, 引用成员, 常量成员. 因为引用成员和常量成员建议在声明的同时进行初始化.

#include <iostream>
using namespace std;

// 必须使用初始化列表的情况
class B{
	public: 
	private:
		int num;
};

class A{
	public: 
		A(int num) : b(), numRef(num), num(num) {  // 初始化列表
		}
	private:
		B b; // 没有默认构造函数的成员
		int &numRef; // 引用成员
		const int num; // 常量成员
};

int main() {
	A a(2);
	return 0;
}

1.4 Destructor (析构函数)

既然有用来初始化类的构造函数, 也有一种用来释放内存/收尾的析构函数.

1.4.1 语法

析构函数和构造函数一样使用类名作为函数名, 但是析构函数函数名之前多一个 ~. 下面示例中, 类实例 myclassmain() 中创建, 被分配在栈上, main() 结束时会被自动销毁, 此时自动调用析构函数.

#include <iostream>
using namespace std;

class MyClass{
	public: 
		MyClass(int a, int b) : a(a), b(b) { cout << "构造函数被调用!" << endl; }
		~MyClass() { cout << "析构函数被调用!" << endl; }  // 析构函数
		void printAB() {
			cout << "a的值是:" << a << ", b的值是:" << b << endl;
		}
	private:
		int a;
		int b;
};

int main() {
	MyClass myclass(2, 3);
	myclass.printAB();
	return 0;
}
-----------------------------------------------------
输出:
构造函数被调用!
a的值是:2, b的值是:3
析构函数被调用!  // `main()` 结束时myclass会被自动销毁, 此时自动调用析构函数.

当然, 析构函数也可以显式调用(高级内容, 待补充). #todo

1.4.2 动态分配对象内存

前面提到 newdelete 可以管理动态内存分配. 对于类的实例, 使用 new 会自动调用构造函数, delete 会自动调用析构函数.

#include <iostream>
using namespace std;

class MyClass{
	public: 
		MyClass(int a, int b) : a(a), b(b) { cout << "构造函数被调用!" << endl; }
		~MyClass() { cout << "析构函数被调用!" << endl; }
		void printAB() {
			cout << "a的值是:" << a << ", b的值是:" << b << endl;
		}
	private:
		int a;
		int b;
};

int main() {
	MyClass *ptr = new MyClass(2, 3); // 调用构造函数
	ptr->printAB();  // 调用成员函数
	delete ptr;  // 调用析构函数
	ptr = new MyClass(6, 5);  // 调用构造函数, 注意, new和delete返回的都是指针
	ptr->printAB();  // 调用成员函数
	delete ptr;  // 调用析构函数
	return 0;
}
------------------------------------------------
输出:
构造函数被调用!
a的值是:2, b的值是:3
析构函数被调用!
构造函数被调用!
a的值是:6, b的值是:5
析构函数被调用!

也可以在类的内部实现成员变量的动态内部分配, 注意, 在构造函数用 new 分配的成员变量, 需要在析构函数用 delete 释放.

#include <iostream>
using namespace std;

class MyClass{
	public: 
		MyClass(int size) : size(size) { 
			arr = new int[size]; // 创建
			for ( int i = 0; i < size; i++ ) {
				arr[i] = i;
			}
		}
		~MyClass() { delete[] arr; }  // 释放
		void printArr() {
			for ( int i = 0; i < size; i++ ) {
				cout << arr[i] << " ";
			}
			cout << endl;
		}
	private:
		int *arr;
		int size;
};
int main() {
	MyClass obj(5);
	obj.printArr();
	return 0;
}
---------------------------------------------------------
输出:
0 1 2 3 4 

1.5 类的作用域

1.5.1 作用域操作符

在类的定义体之外定义成员函数和成员变量需要使用作用域操作符 ::, 表示该成员属于此类, 以避免在两个类分别定义同名成员时发生问题.

#include <iostream>
using namespace std;

class A{
	public:
		A() {}
		static int num;
	private:
};
int A::num = 2;  // 使用作用域操作符
int num = 3;  // 普通的全局变量

int main() {
	cout << "A::num的值为:" << A::num << endl;
	cout << "num的值为:" << num << endl;
	return 0;
}
---------------------------------------------------------------------------------
输出:
A::num的值为:2
num的值为:3

除了对类实例使用成员访问运算符 ., 也可以在一个类A中, 用作用域操作符 :: 使用另一个类B的成员函数:

#include <iostream>
using namespace std;

class B{
	public:
		B() { }
		static void printB() { cout << "print B!" << endl; }
};

class A{
	public:
		A() {}
		void printB() { B::printB(); }  // 用作用域操作符 `::` 使用另一个类B的成员函数
	private:
		int num;
};

int main() {
	A a;
	a.printB();
	return 0;
}
---------------------------------------------------------------------------------
输出:
print B!

1.5.2 名字查找

在使用变量/调用函数时, 编译器需要确认当前变量名/函数名对应的声明是哪一个, 因为复合语句&&函数&&类等等会引入复杂的作用域. 所以如果没处理好声明的顺序/大量使用相同的名字, 可能会使得程序陷入找不到声明&&重定义的问题, 因此有必要了解名字查找的规则.

#include <iostream>
using namespace std;
int main() {
	bool cond = true;
	int a = 2;
	if ( cond ) {
		cout << "a的值是:" << a << endl;  // 输出为2
		int a = 3;
	}
	return 0;
}
class A{
	public:
		A() { num = 0; }  // num定义在后面, 但不会报错. 因为类定义在编译时, 会先编译声明部分, 也就是先跳过此行函数体. 编译完声明后, 才会编译定义, 因为一些成员函数的定义会放在类定义外面.
		NUM getNum() { return num; }  // 和普通情况一样, 编译器找不到NUM, 因为类型名NUM的声明放在了这行后面
		typedef int NUM;
	private:
		int num;
};

int main() {
	A a;
	a.getNum();
	return 0;
}
------------------------------------------------------------------------------
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp:6:17: error: ‘NUM’ does not name a type
    6 |                 NUM getNum() { return num; }
       |                 ^~~
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp: In function ‘int main()’:
/home/siecho/Desktop/zj_obsidian/C++/C++ code/8.5.4.cpp:14:11: error: ‘class A’ has no member named ‘getNum’
   14 |         a.getNum();
       |           ^~~~~~
make[2]: *** [CMakeFiles/case.dir/build.make:76: CMakeFiles/case.dir/C++_code/8.5.4.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/case.dir/all] Error 2
make: *** [Makefile:91: all] Error 2

正如一般的作用域内外的变量相互屏蔽, 类中也可能出现屏蔽成员的情况.

#include <iostream>
using namespace std;

// 屏蔽成员
class Area{
	public: 
		Area(int width, int height) {
			// 用this指针可以避免同名屏蔽
			this->width = width;
			this->height = height;
		}
		// 函数作用域内的参数将同名成员变量屏蔽
		int getArea(int width, int height) { return width * height; }  // `getArea`函数的参数`width`和`height`与类的成员变量`width` and `height`同名. 在函数内部, 参数的作用域优先级高于成员变量, 因此直接使用`width`和`height`时, 访问的是函数参数, 而非类的成员变量(即参数 "屏蔽" 了同名的成员变量).
	private:
		int width;
		int height;
};

int main() {
	Area area(3, 4);
	cout << "长方形面积为:" << area.getArea(5, 6) << endl;
	return 0;
}
-------------------------------------------------------------------
输出:
长方形面积为:30

1.6 静态成员

前面提到,函数的静态变量,生命周期是贯穿整个程序的(与函数内的局部变量不同),作用域仅限于函数内部(与全局变量不同)。

1.6.1 静态成员变量

类的静态成员变量,生命周期和作用域的范围都是类(而不是具体的实例),也就是在类的所有实例对象中都可见。

class Product{
	public: 
		Product(float price) : price(price) {}
		float getPrice() {
			return discountRate * price;
		}
	private:
		float price; 
		static float discountRate;  // 静态成员变量,类内仅声明(加static)
};

// 类外初始化:必须指定类名限定,且只初始化一次 
float Product::discountRate = 0.85;

int main() {
	Product camera(2299.99);
	cout << "相机的最终价格为:" << camera.getPrice() << endl;
	Product tv(1199.99);
	cout << "电视机的最终价格为:" << tv.getPrice() << endl;
	return 0;
}
-----------------------------------------------------------
输出:
相机的最终价格为:1954.99
电视机的最终价格为:1019.99

静态成员变量归属于类,而不是具体的对象,所以要先于任何类对象的创建,并且在全局作用域中进行。创建时使用 static 关键字,初始化时省略,并加一个类的限定符 Product::。它和一般成员变量的区别如下,可以根据使用场景进行选择:

对比维度 非静态成员变量 静态成员变量
关键字 staticfloat discountRate = 0.85; staticstatic float discountRate;
内存存储 每个Product对象(如cameratv)单独存储一份
(内存中存在 2 份0.85
整个Product类只存储一份,所有对象共享
(内存中仅 1 份0.85
数据归属 属于「单个对象」 属于「类本身」,而非某个对象
访问方式 必须通过对象(如camera.discountRate,还需改访问权限) 可通过类名直接访问(Product::discountRate),无需创建对象
修改影响 修改一个对象的discountRate,不影响其他对象 修改Product::discountRate,所有对象立即共享新值

1.6.2 静态成员函数

如果一个成员函数设计为让实例调用显得不合理,就可以尝试设计成静态成员函数,类似地,它也只能通过类名调用,

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

class Student{
	public: 
		Student(int id, string name) : id(id), name(name) {}
		static int generateId() {  // 静态成员函数
			// 静态成员函数只能访问静态成员变量
			return globalID++;
		}
		string getName() {
			return name;
		}
		int getId() {
			return id;
		}
	private:
		string name;
		int id;
		static int globalID;
};

int Student::globalID = 1;

int main() {
	Student stu1generateId(), "小宝";
	cout << stu1.getName() << "的学号为:" << stu1.getId() << endl;
	Student stu2generateId(), "大宝";
	cout << stu2.getName() << "的学号为:" << stu2.getId() << endl;
	return 0;
}
-----------------------------------------------------------
输出:
小宝的学号为:1
大宝的学号为:2

注意,static 成员函数只能访问其他的 static 成员(变量和函数),一般的成员是不能访问的(包括 this 指针

1.6.3 常量静态成员

如上述,类的成员变量需要在构造函数中初始化,而静态成员变量则需要在类中声明,在全局域中初始化。

但是,也可以使用 const static 关键字来声明常量静态成员,它可以直接在类的定义体中初始化:

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

class Student{
	public: 
		Student(string nm, int sc) : name(nm) {
			if ( sc > fullScore ) {
				cout << "分数超出满分,自动调节为100!" << endl;
				score = fullScore;
			} else {
				score = sc;
			}
		}
		string getName() { return name; }
		int getScore() { return score; }
	private:
		string name;
		int score;
		const static int fullScore = 100;   // 可以在类的内部初始化常量静态成员变量
};

int main() {
	Student stu1("小宝", 97);
	cout << stu1.getName() << "的成绩为:" << stu1.getScore() << endl;
	Student stu2("大宝", 105);
	cout << stu2.getName() << "的成绩为:" << stu2.getScore() << endl;
	return 0;
}
-------------------------------------------------------------
输出:
小宝的成绩为:97
分数超出满分,自动调节为100!
大宝的成绩为:100

1.7 Inheritance (继承)

一般称派生类(Derived Class)继承于基类(Dase Class)。

1.7.1 例子

// 交通工具
class Vehicle{
	public:
		Vehicle() { numPassengers = 0; }
		void move() {}
	protected:  //
		int numPassengers; // 乘客数量
};

// 飞机
class Airplane : public Vehicle{  // 继承自交通工具
	public:
		Airplane() {}
};

// 有轮交通工具
class WheeledVehicle : public Vehicle{ // 继承自交通工具
	public:
		WheeledVehicle() {}
};

// 汽车
class Car : public WheeledVehicle{ // 继承自有轮交通工具
	public:
		Car() {}
};

// 自行车
class Bicycle : public WheeledVehicle{ // 继承自有轮交通工具
	public:
		Bicycle() {}
};

int main() {
	return 0;
}

上述代码的基类 Vehicle 的定义中,出现了一个不同于 publicprivate 的关键字 protected,三者的区别如下:

1.7.2 public、protected 和 private

访问权限 可访问者 主要目的
public (公有) 所有人,包括类内部、子类和外部代码。 定义类的公共接口。这些是供类外部用户直接调用的函数或访问的变量。
protected (保护) 类本身的成员、友元以及子类 继承提供支持。允许子类访问父类的成员,但对外部隐藏。
private (私有) 仅限于类本身的成员和友元 实现封装。完全隐藏类的实现细节,外部和子类都无法直接访问。
在继承时,如定义体 class Airplane : public Vehicle{...} 中,使用的 public 关键字表明,子类会继承父类的非私有成员(即 publicprotected)。注意,父类的私有成员不可继承,旨在保证类的封装性。

在继承方面,继承级别和基类访问级别会保留最小值,即:

对于后两者,举两个报错的例子,首先是私有继承:

class Base{
	public:
		Base() : a(0), b(0), c(0) { }
		int a;
	protected:
		int b;
	private:
		int c;
};

class Derived : private Base{  // 私有继承
	public:
		Derived() { d = a + b + c; } // member "Base::c" is inaccessible,它是Base的私有成员
	private:
		int d;
};

class Derived1 : public Derived{
	public:
		Derived1(){}
		int getA() { return a; } // a不可访问,它是Derived的私有成员
		int getB() { return b; } // b不可访问,它是Derived的私有成员
};

int main() {
	Derived1 derived1;
	return 0;
}

然后是保护继承:

class Base{
	public:
		Base() : a(0), b(0), c(0) { }
		int a;
	protected:
		int b;
	private:
		int c;
};

class Derived : protected Base{ // 保护继承
	public:
		Derived() { d = a + b + c; } // // member "Base::c" is inaccessible,c是Base的私有成员
	private:
		int d;
};

class Derived1 : public Derived{
	public:
		Derived1(){}
		int getA() { return a; } // a和b是Derived的保护成员,子类可以访问
		int getB() { return b; }
};

int main() {
	Derived1 derived1;
	derived1.a; // a不可访问,它是Derived1的保护成员
	return 0;
}

1.7.3 Is-a 和 Has-a

有两种继承关系,也就是 Is-a (“是一种”)和 Has-a(“包含一种”)。前者的逻辑关系类似于“汽车是一种交通工具”,后者则是“汽车包含车轮、车门、引擎等等”。

# 1.7.3 #include <vector>
using namespace std;

// 交通工具
class Vehicle{
	public:
		Vehicle() { numPassengers = 0; }
		void move() {}
	protected:
		int numPassengers; // 乘客数量
};

// 轮子
class Wheel{
	public:
		Wheel() { size = 14; }
	private:
		int size; 
};

// 引擎
class Engine{
	public:
		Engine() { capacity = 2000; }
	private:
		int capacity;  // 排量
};

// 汽车
class Car : public Vehicle{  // Car is a Vehicle
	public:
		Car() {}
	protected:
		vector<Wheel> wheels;  // Car has Wheels,使用vector容器存储轮子对象
		Engine engine;  // Car has an Engine
};

int main() {
	return 0;
}

1.7.4 派生类和基类的转换

派生类的对象可以直接访问基类的成员,因为派生类包含基类,并且成员会放在靠前的位置。如果将派生类转换为它的基类,那么派生类自己的成员将会被截断:

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

// 基类
class Base{
	public:
		Base() { b = 0; }
	protected:
		int b;
};

// 派生类
class Derived : public Base{
	public:
		Derived() { d = 0; }
		int d;
};

int main() {
	vector<Base> baseVec;
	Base base;
	Derived derived;
	cout << "derived的大小为:" << sizeof(derived) << endl;
	baseVec.push_back(base);
	baseVec.push_back(derived);
	cout << "放入vector的derived的大小为:" << sizeof(baseVec.back()) << endl;
	// baseVec.back().d 不存在,derived已被截断
	return 0;
}
----------------------------------------------------------------
输出:
derived的大小为:8
放入vector的derived的大小为:4

上述例子中,derived 实例被隐式转换为 Base 类,其成员 d 被截断,导致其 size 只有一个 int 变量的大小(变量 b 的大小)。

如果使用的是派生类的指针,那么就不会被截断:

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

// 基类
class Base{
	public:
		Base() { b = 0; }
	protected:
		int b;
};

// 派生类
class Derived : public Base{
	public:
		Derived() { d = 0; }
		int d;
};

int main() {
	Derived *derived;
	cout << "derived的大小为:" << sizeof(derived) << endl;
	Base *base = new Derived();
	cout << "base指向derived的大小为:" << sizeof(base) << endl;
	return 0;
}
输出:
derived的大小为:8
base指向derived的大小为:8

类的大小

使用 sizeof(类实例) 可以发现:

1.7.5 继承下的构造&&析构函数

定义派生类时,如果省略基类构造函数的显式调用,系统将自动调用默认构造函数。不过以下例子有助于理解这两个函数的调用顺序:

# 1.7.5 #include <iostream>
using namespace std;

// 基类
class Base{
	public:
		Base() : b(0) { cout << "基类的构造函数被调用!" << endl; }
		~Base() { cout << "基类的析构函数被调用!" << endl; }
	protected:
		int b;
};

// 派生类
class Derived : public Base{
	public:
		Derived() { cout << "派生类的构造函数被调用!" << endl; }
		~Derived() { cout << "派生类的析构函数被调用!" << endl; }
	protected:
		int d;
};

int main() {
	Derived derived;
	return 0;
}
--------------------------------------------------------
输出:
基类的构造函数被调用!
派生类的构造函数被调用!
派生类的析构函数被调用!
基类的析构函数被调用!

在初始化类实例时,如果有参数输入构造函数,最好手动调用基类的构造函数:

#include <iostream>
using namespace std;

// 基类
class Base{
	public:
		Base(int bb) : b(bb) { cout << "基类的构造函数被调用!" << endl; }
		~Base() { cout << "基类的析构函数被调用!" << endl; }
	protected:
		int b;
};

// 派生类
class Derived : public Base{
	public:
		Derived(int bb, int dd) : Base(bb), d(dd) { cout << "派生类的构造函数被调用!" << endl; } // 基类的构造函数被调用!
		~Derived() { cout << "派生类的析构函数被调用!" << endl; }
	protected:
		int d;
};

int main() {
	Derived derived(1, 3);
	return 0;
}

1.7.6 (Multiple Inheritance) 多重继承

#include <iostream>
using namespace std;

class Support { // 辅助
	public:
		Support() {}
		void heal() { cout << "发动治疗技能!" << endl; }
		void accelerate() { cout << "发动加速技能!" << endl; }
};

class Fighter {  // 战士
	public:
		Fighter() {}
		void meleeAttack() { cout << "发动近战攻击!" << endl; }
};

class Archer { // 弓箭手
	public:
		Archer() {}
		void rangedAttack() { cout << "发动远程攻击!" << endl; }
};

// 大boss什么都会
// 多重继承的每个基类可以给定不同的访问控制符,这里都用public
class Boss : public Support, public Fighter, public Archer {  // 多重继承
	public:
		// 构造函数需要初始化所有基类
		Boss(): Support(), Fighter(), Archer() {}
		void dodge() { cout << "发动闪避!" << endl; }
		void block() { cout << "发动格挡!" << endl; }
};

int main() {
	Boss shiro;
	shiro.rangedAttack();
	shiro.accelerate();
	shiro.meleeAttack();
	shiro.dodge();
	shiro.meleeAttack();
	shiro.block();
	shiro.heal();
	return 0;
}

1.7.7 显式构造函数

隐式转换会自动调用构造函数,比如下面的例子,将不属于 MyClass 类的数值 5 赋值给类实例 my, 刚好 MyClass 的构造函数接受一个参数,于是就将 5 赋值给了参数 n,在 intMyClass 之间的隐式转换时,自动调用了构造函数:

class MyClass {
	public:
		MyClass(int n) : num(n) {}
		int getNum() { return num; }
	private:
		int num;
};

int main() {
	MyClass my = 5;
	cout << "my中num的值为:" << my.getNum() << endl;
	return 0;
}
----------------------------------------------
输出:
my中num的值为:5

如果不允许这样的构造函数调用,可以在类定义时使用关键字 explicit 关键字来声明构造函数只能显式调用:

class MyClass {
	public:
		explicit MyClass(int n) : num(n) {}  // 显式构造函数
		int getNum() { return num; }
	private:
		int num;
};

int main() { 
	MyClass my = 5;  // error: no suitable constructor exists to convert from "int" to "MyClass"
	cout << "my中num的值为:" << my.getNum() << endl;
	return 0;
}

1.7.8 可变数据成员

#常量成员函数中提到,不能在其函数体中修改成员。如果需要改变成员变量,可以将这样的成员变量声明成 multable 成员,这样它就是可变的:

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

class Product {
	public:
		Product(int i, string n, int p, int w) : id(i), name(n), price(p), weight(w) { views = 0; }
		void checkInfo() const {  // const 成员函数
			cout << "查看商品信息:" << endl;
			cout << "商品号:" << id << endl;
			cout << "商品名:" << name << endl;
			cout << "价格:" << price << "元" << endl;
			cout << "重量:" << weight << "克" << endl;
			cout << "查看次数:" << ++views << endl;  // 查看次数加1
			cout << endl;
		}
	private:
		int id;
		string name;
		int price;
		int weight;
		mutable int views;   // mutable变量
};

int main() {
	Product prod1(1, "辣条", 3, 50);
	Product prod2(2, "辣条", 3, 50);
	prod1.checkInfo();
	prod2.checkInfo();
	prod1.checkInfo();
	prod1.checkInfo();
	return 0;
}
----------------------------------------------------------
输出:
查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:1

查看商品信息:
商品号:2
商品名:辣条
价格:3元
重量:50克
查看次数:1

查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:2

查看商品信息:
商品号:1
商品名:辣条
价格:3元
重量:50克
查看次数:3