结构体区别

structclass仅仅只有默认的访问控制不同

struct默认是public,class默认是private

如果数据只是简单的聚合就用struct,如果用于表示行为和状态的复杂对象,不能随意让外部访问,则用class(这种访问控制的能力就是封装性,类的重要特性)

通过类定义的变量称之为对象

类的操作

访问控制:private, protected, public, friend

用于初始化的函数称为构造函数

析构函数用于对象销毁时做清理,调用时机是对象被销毁时,不管是栈中的对象被自动销毁,还是new出来的对象被手动delete,都会自动调用析构函数

成员函数 在对象内写的函数(只有public才能被外部调用,如果是private只能在成员函数内被调用)

成员函数的this指针指向对象自身,默认会传递

运算符重载(操作符重载)可以让内置的运算符进行重解释,可以将函数名改成对应的操作符,需要加上operator关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BigInt {
int* digits = nullptr;
int size = 0;
public:
BigInt(const string &str, int_sign){
//构造函数
size = str.size();
digits = new int[size];
}
~BigInt(){
//析构函数
}
BigInt operator+(const BigInt &x) {}
BigInt& self_add(const BigInt &x){
return *this;
}
};

一般运算符都是由左右两部分组成的 lhs operator rhs

重载运算符的操作就是右边的操作数auto operator-(const Type &rhs){}

explicit关键字代表必须显式转化,类型转化运算符使用,避免因为二义性带来问题

friend友元,在类中声明并加上友元声明,就可以在其他位置进行访问该类的私有成员,也可以单独对类的某个成员函数声明为友元friend double BigFloat::pow(BigInt&);

成员函数的其他命名方法:BigInt BigInt::add(BigInt &X) {}

继承与多态

继承:继承另一个类的成员变量和函数,方便复用

可访问父类非私有的成员,继承方式影响成员在子类的权限

1
2
3
4
5
6
7
8
9
class Obj {
int x, y, w, h;
int hp, attack;
public:
bool test(Obj* other);
};
class Plant : Obj {
// Plant为子类(派生类) Obj为父类(基类)
}

子类可以访问public和protected成员,不能访问private成员

关键字 ==final== 如果一个类被final修饰,则这个类不能被继承

构造函数无法被继承,需要显式声明,先父再子

析构函数无法被继承,先子后父,建议写成虚函数

纯虚函数不需要实现,但是在子类中必须重写

虚函数的实现原理是虚函数表

虚函数 —> 动态多态

1
2
3
4
5
6
7
8
9
10
11
class Zombie {
public:
virtual void attack () {};
//加上virtual关键字代表是虚函数,才可以在子类中重写
virtual void clicked() = 0;
//纯虚函数,如果一个类有纯虚函数,则不能创建基类对象,只能创建子类对象
};
class Gargantuar : public Zombie {
public:
void attack () override {}; //override代表这个函数是对虚函数的重写
};

例子:高精度大数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <bits/stdc++.h>
using namespace std;


class bint {
int *digits = nullptr;
int size = 0;
void reset(int len) {
if (digits != nullptr) {
delete[] digits;
digits = nullptr;
}
size = len;
digits = new int[size];
}
void trim(){
while(size > 1 && digits[size - 1] == 0) size --;
}
public:
~bint() {
if (digits == nullptr) return;
delete[] digits;
digits = nullptr;
}
bint& operator=(const bint &rhs){
reset(rhs.size);
memcpy(digits, rhs.digits, sizeof(int) * size);
return *this;
}
bint& operator=(const string &rhs){
reset(rhs.length());
size = rhs.size();
digits = new int[size];
for (int i = 0; i < size; i ++) {
digits[i] = rhs[size - 1 - i] - '0';
}
return *this;
}
bool operator<(const bint &rhs) {
if(size != rhs.size) return size < rhs.size;
for(int i = size - 1;i >= 0;i --){
if(digits[i] != rhs.digits[i]){
return digits[i] < rhs.digits[i];
}
}
return false;
}
bint operator-(const bint &rhs){
bint ret;
ret = *this;
for(int i = 0;i < size; i ++){
if (i < rhs.size) ret.digits[i] -= rhs.digits[i];
if(ret.digits[i] < 0) {
ret.digits[i] += 10;
ret.digits[i + 1] --;
}
}
ret.trim();
return ret;
}
bint operator+(const bint &rhs){
bint ret;
ret.size = max(size, rhs.size) + 1;
ret.digits = new int[ret.size];
memset(ret.digits, 0, sizeof(int) * ret.size);
for (int i = 0; i < ret.size - 1; i ++){
if (i < size) ret.digits[i] += digits[i];
if (i < rhs.size) ret.digits[i] += rhs.digits[i];
if (ret.digits[i] >= 10) {
ret.digits[i] -= 10;
ret.digits[i + 1] ++;
}
}
ret.trim();
return ret;
}
friend istream& operator>>(istream &lhs, bint &rhs){
string str;
lhs >> str;
rhs = str;
return lhs;
}
friend ostream& operator<<(ostream &lhs, const bint &rhs){
for (int i = rhs.size - 1; i >= 0; i --) {
lhs << rhs.digits[i];
}
return lhs;
}
};
int main(){
bint a, b;
cin >> a >> b;
if(a < b) {
cout << '-' << b - a << endl;
} else {
cout << a - b << endl;
}
}

重载运算符

在C++中,重载运算符是否加&(引用符号)主要取决于运算符的语义和使用场景。以下是详细说明:

&的情况

  • 返回左值引用的情况

    • 当重载的运算符需要返回一个左值(可以被赋值的对象)时,通常会加&。例如,重载赋值运算符=时,通常返回一个左值引用,以便支持链式赋值。
      1
      2
      3
      4
      MyClass& operator=(const MyClass& rhs) {
      // 实现赋值操作
      return *this;
      }
      这样可以支持如下链式赋值:
      1
      2
      MyClass a, b, c;
      a = b = c;
    • 重载下标运算符[]时,也通常返回左值引用,以便可以对返回的对象进行赋值操作。
      1
      2
      3
      4
      int& operator[](size_t index) {
      // 返回数组元素的引用
      return data[index];
      }
      这样可以支持如下操作:
      1
      obj[0] = 10; // 通过下标运算符返回的引用进行赋值
  • 避免不必要的拷贝
    • 当重载运算符的返回值是一个较大的对象时,为了避免不必要的拷贝,可以返回引用。例如,重载输入运算符>>时,通常返回istream&,以避免拷贝输入流对象。
      1
      2
      3
      4
      istream& operator>>(istream& in, MyClass& obj) {
      // 从输入流读取数据到obj
      return in;
      }

不加&的情况

  • 返回右值的情况
    • 当重载的运算符需要返回一个临时对象(右值)时,不加&。例如,重载加法运算符+时,通常返回一个临时对象。
      1
      2
      3
      4
      5
      MyClass operator+(const MyClass& rhs) const {
      MyClass temp(*this);
      temp += rhs;
      return temp;
      }
      这里返回的是一个临时的MyClass对象,而不是引用。
  • 返回布尔值的情况
    • 当重载的运算符返回布尔值时,也不加&。例如,重载比较运算符==!=等。
      1
      2
      3
      4
      bool operator==(const MyClass& rhs) const {
      // 比较逻辑
      return true; // 或 false
      }
  • 返回指针的情况
    • 当重载的运算符返回指针时,也不加&。例如,重载成员访问运算符->时,返回一个指针。
      1
      2
      3
      MyClass* operator->() {
      return this;
      }

特殊情况

  • 重载下标运算符[]的常量版本
    • 如果重载的下标运算符是常量成员函数,需要返回一个常量引用,以保证返回的对象不能被修改。
      1
      2
      3
      const int& operator[](size_t index) const {
      return data[index];
      }
    • 这样可以防止通过下标运算符修改对象的成员变量。

总结

  • &:当需要返回左值引用(如赋值运算符、下标运算符等)或者避免不必要的拷贝(如输入运算符)时。
  • 不加&:当返回临时对象(如加法运算符)、布尔值、指针等情况时。

根据具体的运算符语义和使用场景选择是否加&,可以更好地实现代码的效率和语义正确性。

动态联编

在面向对象编程中,动态联编(Dynamic Binding) 是一种机制,允许在运行时根据对象的实际类型来调用相应的函数。基类指针动态联编调用派生类的函数,通常涉及到 多态(Polymorphism)虚函数(Virtual Function)。以下为你详细解释并举例说明:

基本概念

  • 基类与派生类 :基类是定义了一些通用属性和行为的类,派生类是从基类继承而来的类,可以继承基类的成员,并且可以添加新的成员或重写(Override)基类的成员函数。
  • 虚函数 :在基类中声明为 virtual 的函数。当通过基类指针或引用调用虚函数时,会根据对象的实际类型来调用相应的函数,而不是根据指针或引用的类型。这是实现动态联编的关键。
  • 动态联编 :在运行时根据对象的实际类型来确定调用哪个函数。与之相对的是静态联编(Static Binding),静态联编是在编译时就确定调用哪个函数。

举例说明

假设有一个基类 Animal 和两个派生类 DogCat,基类中有一个虚函数 makeSound(),派生类重写了这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;

// 基类
class Animal {
public:
virtual void makeSound() {
cout << "Animal makes a sound" << endl;
}
};

// 派生类 Dog
class Dog : public Animal {
public:
void makeSound() override {
cout << "Dog barks" << endl;
}
};

// 派生类 Cat
class Cat : public Animal {
public:
void makeSound() override {
cout << "Cat meows" << endl;
}
};

int main() {
// 创建基类指针
Animal* animalPtr;

// 创建派生类对象
Dog dog;
Cat cat;

// 将基类指针指向派生类对象
animalPtr = &dog;
animalPtr->makeSound(); // 输出 "Dog barks"

animalPtr = &cat;
animalPtr->makeSound(); // 输出 "Cat meows"

return 0;
}

在这个例子中:

  • Animal 是基类,DogCat 是派生类。
  • makeSound() 是基类中的虚函数,DogCat 分别重写了这个函数。
  • main() 函数中,创建了一个基类指针 animalPtr,然后分别将它指向 DogCat 对象。
  • 当通过 animalPtr 调用 makeSound() 函数时,会根据 animalPtr 指向的对象的实际类型(DogCat)来调用相应的函数,而不是调用基类 Animal 中的 makeSound() 函数。这就是动态联编的体现。

如果 makeSound() 函数不是虚函数,那么通过基类指针调用函数时,就会调用基类中的函数,而不会根据对象的实际类型调用派生类中的函数,这就是静态联编的行为。