C++面向对象速览(二)

23 阅读10分钟

运算符重载之章

加法运算符重载39

1,成员函数重载

2,全局函数重载

运算符重载主要用于自定义类型运算,也可以使用函数重载。

class Person{
    public:
    int m_a;
    int m_b;
    
    Person operator+ (Person &p){
        Person temp;
        temp.m_a = this -> m_a + p.m_a;
        temp.m_b = this -> m_b + p.m_b;
        return temp;
    }
    
    Person operator+ (Person &p ,int num){
        Person temp;
        temp.m_a = this -> m_a + num;
        temp.m_b = this -> m_b + num;
        return temp;
    }
}
​
Person operator+ (Person &p1, Person &p2){
    Person temp;
    temp.m_a = p1 -> m_a + p2.m_a;
    temp.m_b = p2 -> m_b + p1.m_b;
    return temp;
}
​
void test01(){
    Person p1;
    p1.m_a = 10;
    p1.m_b = 10;
    Person p2;
    p2.m_a = 10;
    p2.m_b = 10;
    
    Person p3 = p1 + p2;
    Person p4 = p1+100;
}

左移运算符重载40

不用成员函数做左移运算符重载的原因是格式不好看(无法实现cout在左侧),用去全局函数做重载,传入ostream对象cout,和类对象,再应用一些链式编程思想,即可完成连续输出。

class Person{
public:
    int age;
    string name;
    //p.operator<<(p)
    (x)Person operator<< (Person &p);
    //p << cout
    (x)Person operator<< (ostream &cout);
}
//standard out stream object only one in whole situation
ostream & operator<< (ostream & cout , Person & p){
    cout<<"age : "<<p->age<<" Name : "<< o->name <<endl;
    return cout;
}

自增/自减运算符重载41

要是没有黑马的教程我根本就注意不到这二者的区别:

int a = 10;
--(--a);
(x)(a--)--;
表达式返回类型是否合法原因
--(--a)左值合法可以对左值连续递减
(a--)--右值非法不能对右值递减
  • 核心区别:前置递减返回左值,后置递减返回右值。
  • 设计逻辑:后置递减需要返回原始值(临时值),因此不能支持连续递减。
class Czpp {
    friend ostream& operator<< (ostream& cout, Czpp c01);
public:
    Czpp() {
        o = 0;
    }
    //the reason of add & becaues ++op is design for continuous output.
    //So we can call ++(++a) trustingly
    Czpp&  operator++ () {
        o++;
        return *this;
    }
​
    Czpp operator++ (int) {
        Czpp temp = *this;
        o++;
        return temp;
    }
​
private:
    int o;
};
​
ostream& operator<< (ostream& cout, Czpp c01) {
    cout << c01.o << endl;
    return cout;
}
​
void testcase(){
    Czpp c01;
    cout << ++(++c01) << endl;
    cout << c01 << endl;
    cout << ((c01++)++)++ << endl;
    cout << c01 << endl;
}
​
​
class Czmm {
​
    friend  ostream& operator<< (ostream& cout, Czmm c01);
​
public:
    Czmm() {
        oi = 100;
    }
​
    Czmm(int n) {
        oi = n;
    }
​
    Czmm & operator-- () {
        oi
            --;
        return *this;
    }
​
    Czmm operator--(int) {
        Czmm temp = *this;
        oi--;
        return temp;
    }
private:
    int oi;
};
​
​
ostream& operator<< (ostream& cout, Czmm c01) {
    cout << c01.oi << endl;
    return cout;
}
​
void test02() {
    Czmm c02;
    cout << --c02 << endl;
    cout << --(--c02) << endl;
    Czmm c03(20);
    cout << c03-- << endl;
    cout << c03 << endl;
}

赋值运算符重载42

C++编译器会给一个类添加四个函数:

1,默认构造函数,函数体为空。

2,默认析构函数,函数体为空。

3,默认拷贝构造函数,对属性进行值拷贝。

4,赋值运算符 operator= 对属性进行值拷贝。

我们在类外进行类的等号赋值操作时,进行的是浅拷贝,容易触发地址重复释放问题。可以通过重载赋值运算符进行深拷贝规避问题。

class Person{
public:
    int* m_age;
    
    Person(int age){
        m_age = new int(age);
    }
    
    
    //add operator= heavyload
    
    //chain programming ideas
    Person & operator= (Person& p){
        //Compiler offer shallow copy
        //m_age = p.m_age;
        
        //Defensive programming
        //Chenking if this property is in heap area
        if(m_age!=NULL){
            delete m_age;
            m_age = NULL;
        }
        
        m_age = new int (*p.m_age);
        return *this;
    }
    
    ~Person(){
        if(m_age != NULL){
            delete m_age;
            m_age = NULL;
        }
    }
};
​
void test01(){
    Person p1(18);
    Person p2(20);
    
    //assignment operatation will trigger shallow copy
    //Repetition release heap area memory
    p1 = p2;
    
    cout<<*p1.m_age<<endl;
    cout<<*p2.m_age<<endl;
    
}

关系运算符重载43

涉及>,<,==,!=等,返回bool类型值。

函数调用重载运算符&匿名函数对象44

函数调用()也可以重载

class Myprint{
    public:
    void operator()(string rest){
        cout<<rest<<endl;
    }
};
​
void test01(){
    Myprint mp;
    mp("hello world");
    //anonymous func object
    Myprint()("hello world");
}

继承之章

初识继承45

公共部分抽象为父类,子类继承

继承方式46

image-20250329221827552

继承中的对象模型47

子类继承父类,会得到所有父类成员变量,只是会自动隐藏父类的私有成员变量。通过vs Developer Command Prompt查看(报告单个类的布局)

cl /d1 reportSingleClassLayout类名 “cpp文件名”

继承中的构造以及析构顺序48

父类先构造,后析构

同名成员处理49

如果子类中出现和父类同名的成员函数,子类的同名成员函数会自动隐藏掉父类中所有的同名成员函数,在有子类的情况下,(即使有能明显区分的重载也不行)访问父类中被隐藏的同名成员函数,需要加作用域。

同名静态成员处理50

1,通过对象访问

2,通过类名访问

class father {
public:
    int fapuint;
    static int fapusint;
protected:
    int faprint;
    static int faprsint;
private:
    int fapriint;
    static int faprisint;
​
public:
​
    father() {
        fapuint = 10;
        //I forget how to init static ^-^
        /*fapusint = 100;*/
        faprint = 20;
        fapriint = 30;
    }
​
    void pint() {
        cout << "father func" << endl;
    }
​
    static void spint() {
        cout << "father static func" << endl;
    }
​
};
​
int father::fapusint = 100;
​
class son : public father {
​
public:
    static int fapusint;
​
    void pint() {
        cout << "son func" << endl;
    }
​
    static void spint() {
        cout << "son static func" << endl;
    }
};
​
int son::fapusint = 100;
​
void tcinher() {
    son s1;
    //access from obj 
    cout << "son ' s varible  access with obj" << s1.fapuint << endl;
    cout << "fa ' s varible  access with obj" << s1.father::fapuint << endl;
    //access from action scope(class name)
    //This access way is only ok to static varible / func.
    cout << "son ' s static varible  access with class name" << son::fapusint << endl;
    cout << "fa ' s static varible  access with class name" << son::father::fapusint << endl;
​
    //son ' s func access with obj
    s1.pint();
    //fa ' s func access with obj
    s1.father::pint();
    //son ' s static func  access with class name
    son::spint();
    //fa ' s static func  access with class name
    son::father:: spint();
}

继承语法51

多继承,注意以作用域区分同名。

class son : public base1, public base2{
    
};

菱形继承52

虚基类,虚继承

情况继承方式grandfa 子对象数量访问 m_age 是否二义
普通继承class fath : public grandfa2 份(fath + math是(需指定路径)
虚继承class fath : virtual public grandfa1 份(共享)否(可直接访问)
  • fathmath 共享同一个 grandfa 子对象
  • sons 只包含 一个 grandfa 实例,避免了二义性。
底层实现(编译器如何支持虚继承?)

虚继承的实现通常依赖 虚基类表(Virtual Base Table, vbtable)

  1. 虚基类指针(vbptr)

    • 当某个类虚继承自另一个类时,编译器会为该类添加一个 虚基类指针(vbptr) ,指向虚基类表(vbtable)。
  2. 虚基类表(vbtable)

    • 存储了虚基类相对于当前对象的偏移量。
    • 这样,即使 fathmathsons 中的位置不同,它们也能正确找到共享的 grandfa 子对象。
class grandfa {
public:
    int m_age;
​
};
//class uncle : virtual public grandfa
class uncle : public grandfa {
public:
​
};
//class math : virtual public grandfa
class math : public grandfa {
public:
​
};
//class fath : virtual public grandfa
class fath :  public grandfa {
public:
​
};
​
class sons :public fath, public math , public uncle {
public:
​
};
​
void testv() {
    sons son1;
    
    son1.uncle::m_age = 77;
    son1.math::m_age = 19;
    /*son1.fath::m_age = 20;*/
    son1.grandfa::m_age = 100;
​
    //cout << "son's age = " <<son1.m_age << endl;
    cout << "uncle's age = " << son1.uncle::m_age << endl; // 77(未被修改)
    /*cout << "fath's age = " << son1.fath::m_age << endl;*/
    cout << "math's age = " << son1.math::m_age << endl;   // 19(未被修改)
    cout << "gf's age = " << son1.grandfa::m_age << endl;  // 100(修改的是 fath 的版本)
    
}
​

实际上我在看到多态原理剖析时,用sizeof测试了各个类的大小:

cout << "size of gf " << sizeof(grandfa) << endl;//4
cout << "size of uncle " << sizeof(uncle) << endl;//16
cout << "size of math " << sizeof(math) << endl;//16
cout << "size of fath " << sizeof(fath) << endl;//16
cout << "size of sons " << sizeof(sons) << endl;//32

Deepseek声称可能存在内存对齐,虚基类指针占用8位,对齐4位,继承的int4位。

在不使用虚继承时出现的情况:

  1. son1.grandfa::m_age = 100;模糊访问,但某些编译器(如 MSVC)会 隐式选择第一个基类(fath)的 grandfa,因此:

    • 它修改的是 fathgrandfa::m_age(但 fath::m_age 之前未被赋值,可能是随机值或 0)。
    • uncle::m_agemath::m_age 未被影响,仍然是 7719
  2. 输出时:

    • son1.uncle::m_age77(未被 grandfa::m_age = 100 影响)。
    • son1.math::m_age19(未被影响)。
    • son1.grandfa::m_age100(修改的是 fath 的版本)。

问题:为什么 grandfa::m_age = 100 不影响 uncle::m_agemath::m_age

因为:

  • sons 有 3 份独立的 grandfa 子对象fathmathuncle 各一份)。
  • son1.grandfa::m_age 只能修改其中一份(具体是哪份取决于编译器,这里是 fath 的)。
  • uncle::m_agemath::m_age 是另外两份,不受影响。

EX:继承中的父子类思考:

为什么子类构造时要求父类必须有默认构造函数?

在 C++ 中,子类对象的构造过程一定会先构造父类部分。如果父类没有默认构造函数(无参构造函数),而子类的构造函数又没有显式调用父类的某个构造函数,编译器就无法自动构造父类,从而导致编译错误。


1. 构造顺序:父类 → 子类

当创建一个子类对象时,构造顺序是:

  1. 父类的构造函数(如果父类还有父类,则继续向上递归)
  2. 子类成员变量的构造函数(按声明顺序)
  3. 子类自己的构造函数

如果父类没有默认构造函数,而子类又没有显式指定调用父类的哪个构造函数,编译器就不知道如何构造父类部分,因此会报错。


2. 示例分析
❌ 错误情况:父类没有默认构造函数,子类未显式调用父类构造函数
class Parent {
public:
    Parent(int x) { }  // 只有带参数的构造函数,没有默认构造函数
};
​
class Child : public Parent {
public:
    Child() { }  // 错误!编译器不知道如何构造 Parent
};
​
int main() {
    Child c;  // 编译失败
    return 0;
}

报错信息:

error: no matching function for call to 'Parent::Parent()'

原因: Child() 构造函数没有显式指明如何构造 Parent,编译器尝试调用 Parent::Parent()(默认构造函数),但 Parent 没有默认构造函数,因此报错。


✅ 正确做法:
(1)父类提供默认构造函数
class Parent {
public:
    Parent() { }       // 提供默认构造函数
    Parent(int x) { }  // 也可以有其他构造函数
};
​
class Child : public Parent {
public:
    Child() { }  // 现在可以编译,自动调用 Parent()
};
(2)子类显式调用父类的某个构造函数
class Parent {
public:
    Parent(int x) { }  // 只有带参数的构造函数
};
​
class Child : public Parent {
public:
    Child() : Parent(42) { }  // 显式调用 Parent(int)
};
​
int main() {
    Child c;  // 正确,Parent 被构造为 Parent(42)
    return 0;
}

关键点:

  • Child() : Parent(42)成员初始化列表(Member Initializer List) ,在这里显式调用了 Parent(int)
  • 如果不写 : Parent(42),编译器会尝试调用 Parent(),但 Parent 没有默认构造函数,导致编译错误。

3. 更深层次的情况:多重继承

如果子类继承多个父类,且某些父类没有默认构造函数,必须在子类构造函数中显式调用它们的构造函数:

class Parent1 {
public:
    Parent1(int x) { }
};
​
class Parent2 {
public:
    Parent2(int y) { }
};
​
class Child : public Parent1, public Parent2 {
public:
    Child() : Parent1(10), Parent2(20) { }  // 必须显式调用
};

如果不写 : Parent1(10), Parent2(20),编译器会报错!


4. 为什么 C++ 这样设计?
  • 明确性:强制程序员显式指定如何构造父类,避免意外行为。
  • 灵活性:允许父类的构造依赖子类传递的参数。
  • 避免二义性:如果父类有多个构造函数,编译器不知道默认用哪个,必须由程序员指定。

总结
情况父类构造函数子类构造函数写法是否合法
父类有默认构造函数Parent() 存在Child() { }✅ 合法
父类无默认构造函数只有 Parent(int)Child() { }❌ 编译错误
父类无默认构造函数只有 Parent(int)Child() : Parent(42) { }✅ 合法

关键规则:

  • 如果父类没有默认构造函数,子类必须显式调用父类的某个构造函数
  • 如果父类有默认构造函数,子类可以不显式调用(编译器自动调用 Parent())。