1. 重载

1.1 函数重载

• 在同一个作用域内,可以声明几个功能类似的同名函数
• 这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。
• 下面的实例中,同名函数 print() 被用于输出不同的数据类型:
#include <iostream>
#include <string>


using namespace std;

class printData {
public:
    void print(int data);
    void print(double data);
    void print(string data);
};

void printData :: print(int data){
    cout << "输出的是整型数:" << data << endl;
}

void printData :: print(double data){
    cout << "输出的是浮点数:" << data << endl;
}

void printData :: print(string data){
    cout << "输出的是字符串:" << data << endl;
}
int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    printData *p = new printData();
    p->print(1);
    p->print(1.1);
    p->print("Hello World!");
    return 0;
}

1.2 运算符重载

• 在C++中,运算符重载是一个允许自定义各种运算符(如 + , - , == , != 等)在自定义类型(类或结构体)上的行为特性。这意味着你可以定义类似于内置类型的运算符行为,使你的自定义类型更加
直观和易于使用。
• 基本原则
        1. 不可以创建新的运算符:只能重载已经存在的运算符。
        2. 至少有一个操作数是用户定义的类型:不能重载两个基本类型的运算符。
        3. 不能更改运算符的优先级:重载的运算符保持其原有的优先级和结合性。
• 示例1:假设我们有一个Person 类,我们可以重载 == 运算符来实现两个Person是否相等的判断。
#include <iostream>
#include <string>


using namespace std;

class Person{
public:
    string name;
    int id;

    bool operator==(Person tmp);
};

bool Person::operator==(Person tmp){
    return tmp.name == name && tmp.id == id;//此时,重载之后,这个类才有==这样的比较。
}


int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    //创建两个人比较
//    Person p1;
//    p1.name = "jack";
//    p1.id = 1;

//    Person p2;
//    p2.name = "tom";
//    p2.id = 2;

    Person p1;
    p1.name = "jack";
    p1.id = 1;

    Person p2;
    p2.name = "jack";
    p2.id = 1;

    // bool ret = p1 == p2,直接将两个对象对比p1 == p2,
    //会报错,因为Person类没有定义 == 运算符
    //所以需要进行运算符重载。

    bool ret = p1 == p2;
    cout << "比较的结果是:" << ret << endl;

    return 0;
}

• 示例2:假设我们有一个简单的 Point 类,我们可以重载 + 运算符来实现两个点的加法。

#include <iostream>

using namespace std;

class Point{
public:
    int x,y;

    Point operator+(Point tmp){
        Point ret;
        ret.x = tmp.x + x;
        ret.y = tmp.y + y;

        return ret;
    }


};



int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    Point p1;
    p1.x = 1;
    p1.y = 2;

    Point p2;
    p2.x = 2;
    p2.y = 3;

    //不用重载的情况
//    Point p3;
//    p3.x = p2.x + p1.x;
//    p3.y = p2.y + p1.y;

    Point p3 = p1 + p2;
    cout << "p3的x为" << p3.x << ",p3的y为:" << p3.y << endl;
    return 0;
}

2. 构造函数

2.1 什么是构造函数

• 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
• 那构造函数的作用是初始化成员变量,加载内存空间等
• 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

2.2 构造函数的分类

• 默认的构造函数没有任何参数(无参构造函数),但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。

2.3 初始化列表

• 在C++中,使用初始化列表来初始化类的字段是一种高效的初始化方式,尤其在构造函数中。初始化列表直接在对象的构造过程中初始化成员变量而不是先创建成员变量后再赋值
• 初始化列表紧跟在构造函数参数列表后面,以冒号( : )开始,后跟一个或多个初始化表达式,每个表达式通常用逗号分隔。下面是使用初始化列表初始化字段的例子:
#include <iostream>
#include <string>

using namespace std;

class MyClass{
private:
    int x;
    string y;
public:
    MyClass(int id,string name) : x(id),y(name){

    }

    void printinfo(){
        cout << "id为:" << x << "名字为:" << y;
    }
};

int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    MyClass(1,"tom").printinfo();
    return 0;
}
• 在这个例子中, MyClass 有两个成员变量:x(int 类型), y(string类型)。当创建 MyClass 的一个实例时,我们通过构造函数传递两个参数,这些参数被用于通过初始化列表直接初始化成员变量。初始化列表 : x(id), y(name)的意思是用 id初始化x,用name初始化 y 
• 初始化列表的优点包括:
1. 效率:对于非基本类型的对象,使用初始化列表比在构造函数体内赋值更高效,因为它避免了先默认构造然后再赋值的额外开销。
2. 必要性:对于引用类型和常量类型的成员变量必须使用初始化列表,因为这些类型的成员变量在构造函数体内不能被赋值。
3. 顺序成员变量的初始化顺序是按照它们在类中声明的顺序,而不是初始化列表中的顺序

2.4 this关键字

• this是一个特殊的指针,指向当前对象的地址作用域是类的非静态成员函数内部。

2.5 new关键字

• 在C++中, new关键字用于动态分配内存。它是C++处理动态内存分配的主要工具之一,允许在程序运行时根据需要分配内存。
• delete配对使用:使用new分配的内存必须显式地通过delete(对于单个对象)或delete[](对于数组)来释放,以避免内存泄露。
• 注意事项
• 异常安全:如果new分配内存失败,它会抛出std::bad_alloc异常(除非使用了 nothrow 版本)。
• 内存泄露:忘记释放使用new分配的内存会导致内存泄露。
• 匹配使用 delete delete[]:为避免未定义行为,使用new分配的单个对象应该使用delete释放,使用new[]分配的数组应该使用delete[]释放。

3. 析构函数

3.1 什么是析构函数

• 析构函数是C++中的一个特殊的成员函数,它在对象生命周期结束时被自动调用用于执行对象销毁前的清理工作。析构函数特别重要,尤其是在涉及动态分配的资源(如内存、文件句柄、网络连接等)的情况下。
• 基本特性
        1. 名称:析构函数的名称由波浪号( ~ )后跟类名构成,如 ~MyClass()
        2. 无返回值和参数:析构函数不接受任何参数,也不返回任何值。
        3. 自动调用:对象的生命周期结束时(例如,一个局部对象的作用域结束,或者使用 delete 删除一个动态分配的对象),析构函数会被自动调用。
        4. 不可重载:每个类只能有一个析构函数。
        5. 继承和多态:如果一个类是多态基类,其析构函数应该是虚的。
• 示例
#include <iostream>
#include <cstdlib>
using namespace std;

class MyClass{
private:
    int* datas;
public:
    MyClass(int size){
        datas = new int[size];
    }

    ~MyClass(){
        cout << "析构函数被调用" << endl;
        delete[] datas;
    }
};

int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    MyClass m1(5);
    MyClass *m2 = new MyClass(10);
    delete m2;
    _sleep(5);
    cout << "hello world" << endl;
    _sleep(5);
    return 0;
}
//执行结果是
//析构函数被调用
//hello world
//析构函数被调用

• 执行过程:

• 第一次执行"析构函数被调用" 这是因为在堆上面分配完内存之后,执行delete这个操作,触发了析构函数的执行。

• 第二次执行"析构函数被调用" 这是因为m1在离开main栈作用域后,自动触发了析构函数的执行。

3.2 析构函数的要点

要点 描述
定义和作
析构函数在对象生命周期结束时自动调用,用于清理对象可能持有的资源。
语法
析构函数名称由波浪线 (~) 后跟类名构成,例如 MyClass 的析构函数为~MyClass() 。
资源管理
用于释放对象在生命周期中分配的资源,如动态内存、文件句柄、网络连接等。
自动调用机制
当对象离开其作用域或通过 delete 删除时,将自动调用其析构函数。
防止资源泄露
正确实现析构函数对防止资源泄露至关重要,特别是在涉及动态资源分配的情况。
序析构函数
如果类作为基类设计,应有一个虚析构函数,以确保正确调用派生类的析构函数。
析构函数与异常
析构函数不应抛出异常,如果可能抛出,应在函数内捕获。
删除的析构函数
可以通过将析构函数声明为删除( ~MyClass() = delete; )来禁止删除某类对象。
与析构函数的关系
每个类只能有一个析构函数,不可重载,与构造函数相比。

4. 静态成员

4.1 静态成员的定义

• 静态成员在C++类中是一个重要的概念,它包括静态成员变量和静态成员函数。静态成员的特点和存在的意义如下:
• 静态成员变量
1. 定义:静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建了多少个类的实例,静态成员变量只有一份拷贝(静态成员只加载一次)。
2. 初始化:静态成员变量需要在类外进行初始化,通常在类的实现文件中。
3. 访问:静态成员变量可以通过类名直接访问,不需要创建类的对象。也可以通过类的对象访问。
4. 用途:常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共享。
• 静态成员函数
1. 定义:静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和非静态成员函数。
2. 访问:类似于静态成员变量,静态成员函数可以通过类名直接调用,也可以通过类的实例调用。
3. 用途常用于实现与具体对象无关的功能,或访问静态成员变量
• 存在的意义
• 共享数据:允许对象之间共享数据,而不需要每个对象都有一份拷贝。
• 节省内存:对于频繁使用的类,使用静态成员可以节省内存。
• 独立于对象的功能:静态成员函数提供了一种在不创建对象的情况下执行操作的方法,这对于实现工具函数或管理类级别状态很有用。
• 示例:
#include <iostream>

using namespace std;

class MyClass{
public:
    static int value;

    MyClass(){
        this->value++;
    }

    ~MyClass(){
        this->value--;
    }
    static int getStaticInstance(){
        return  value;
    }

};
//静态成员变量需要在类外进行初始化,通常在类的实现文件中
int MyClass :: value = 0;
int main()
{
    system("chcp 65001 > nul");//强制控制台用UTF-8编码
    MyClass p1,p2;
    //cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
    cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
    MyClass *p3 = new MyClass();
    //cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
    cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
    delete p3;
    //cout << "当前类MyClass有" << MyClass::value << "个实例" << endl;
    cout << "当前类MyClass有" << MyClass::getStaticInstance() << "个实例" << endl;
    return 0;
}

5. 继承

5.1 基础的基本概念

• 继承是面向对象编程(OOP)中的一个核心概念,特别是在C++中。它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码重用,以及建立一种类型之间的层次关系。
• 特点
        1. 代码重用:子类继承了父类的属性和方法,减少了代码的重复编写。
        2. 扩展性:子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
        3. 多态性:通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数
• 基本用法
        •在C++中,继承可以是公有(public)、保护(protected)或私有(private)的,这决定了基类成员在派生类中的访问权限

5.2 权限对继承的影响

• 在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承(public 、 protected private)如何影响基类的不同类型成员(public protected 、 private)在派生类中的访问级别。
基类成员类型 public继承 protected继承 private继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问
• 解释:
public继承:基类的public成员在派生类中仍然是public的, protected 成员仍然是protected的。基类的private成员在派生类中不可访问。
protected继承:基类的publicprotected成员在派生类中都变成protected的。基类的private成员在派生类中不可访问。
private继承:基类的publicprotected成员在派生类中都变成 private的。基类的private成员在派生类中不可访问。
• 基类的private成员始终不可直接在派生类中访问!!!!!
• 访问权限
访问权限 类内部 同一个类的对象 子类 类外部
public
✔️ 可访问
✔️ 可访问
✔️ 可访问
✔️ 可访问
protected
✔️ 可访问
不可访问
✔️ 可访问
不可访问
private
✔️ 可访问
不可访问
不可访问
不可访问

• 示例

#include <iostream>

using namespace std;

//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public://公有数据
    string type;
    string contry;
    string color;
    double price;
    int numOfWheel;
protected://受保护数据
    int protectedData;
private://私有数据
    int privateData;
public:
    void run(){
        cout << "车跑起来了" << endl;
    }
    void stop();
};

//私有继承测试
//私有继承了父类,所以public和protected修饰的成员都变成private
//而原本private修饰的成员是不可访问的。
class TestClass : private Vehicle{
public:void tsetFunc(){
        //基类的公有数据被私有继承后,在子类中权限变成私有,只限在本类使用,
        //此时这个price是私有的。
        price = 10;
    }
};

//保护继承测试
//受保护继承父类,所以public和protected修饰的成员变成protected修饰符修饰的成员
//而原本private修饰的成员是不可访问的。
class Truck : protected Vehicle{
public:
    void testFunc(){
        type = "数据测试"; //变成protected权限。
        protectedData = 10; //还是protected受保护权限
        privateData = 10; //报错了,基类的私有成员,不管哪种方式的继承都是不可访问的。
    }
};

//公有继承测试
//公有继承父类,所以public和protected修饰的成员都还是原来的修饰符
//而原本private修饰的成员是不可访问的。
class Bickle : public Vehicle{
public:
    void testFunc(){
        protectedData = 10;
    }
};

//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:
    int stateOfTop;
    void openTopped();
    void pdrifting();
};
int main()
{
    TestClass test;
    test.price = 3.3; //报错了,基类的公有成员被私有继承后,降为私有权限

    Truck t;
    t.type = "测试"; //报错了,基类的公有成员被保护继承后,降为保护权限,因为protected修饰的成员不能在类外部使用
    t.protectedData = 10; //从报错信息看出,保护继承造成基类的保护成员还是保持保护权限
    
    Roadster ftype;
    ftype.type = "捷豹Ftype";
    ftype.run();
    
    Bickle bike;
    bike.type = "死飞";
    bike.run();
    return 0;
}


5.3 基类构造函数

• 在C++中,子类可以通过其构造函数的初始化列表来调用基类的构造函数。这是在构造子类对象时初始化基类部分的标准做法。 当创建子类的对象时,基类的构造函数总是在子类的构造函数之前被调用。如果没有明确指定,将调用基类的默认构造函数(无参构造方法)。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,需要在子类构造函数的初始化列表中明确指定

5.4 虚函数

• 在C++中, virtualoverride关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。
 virtual 关键字
1. 使用场景在基类中声明虚函数
2. 目的:允许子类重写该函数,实现多态。
3. 行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本
• override 关键字
1. 使用场景:在派生类中重写虚函数。
2. 目的:明确指示函数意图重写基类的虚函数。
3. 行为:确保子类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错
• 注意点
• 只在子类中使用override:override应仅用于子类中重写基类的虚函数。
• 虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。
• 默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual
关键字才会成为虚函数。
• 继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有子类中自动成为虚函数,无论是否使用 virtual 关键字。

5.5 多重继承

• 在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着子类可以继承多个基类的属性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同的成员时。
• 基本概念
        • 在多重继承中,子类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名称的成员,则必须明确指出所引用的是哪个基类的成员。
• 注意事项
• 菱形继承问题:如果两个基类继承来自同一个更高层的基类,这可能导致子类中存在两份基类的副本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
• 复杂性:多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时
• 设计考虑:虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些情况下,使用组合或接口(纯虚类)可能是更好的设计选择。

5.6 虚继承

• 虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一个类继承自两个具有共同基类的类时,会导致共同基类的成员在子类中存在两份拷贝,这不仅会导致资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来解决这一问题。
• 菱形继承的示例
#include <iostream>

using namespace std;

class Base {
public:
    int data;
};
class Derived1 : public Base {
    // 继承自 Base
};
class Derived2 : public Base {
    // 继承自 Base
};
class FinalDerived : public Derived1, public Derived2 {
    // 继承自 Derived1 和 Derived2
};

int main()
{
    FinalDerived f;
   // f.data;这样直接调用报错
    return 0;
}
• 在这个例子中, FinalDerived 类通过Derived1Derived2间接地继承自 Base 类两次。因此,
它包含了两份 Base的成员拷贝(有两份data)。
• 使用虚继承解决菱形继承问题
#include <iostream>

using namespace std;

class Base {
public:
    int data;
};
class Derived1 : virtual public Base {
    // 继承自 Base
};
class Derived2 : virtual public Base {
    // 继承自 Base
};
class FinalDerived : public Derived1, public Derived2 {
    // 继承自 Derived1 和 Derived2
};

int main()
{
    FinalDerived f;
    f.data;//这样直接调用报错,但是使用了虚继承之后就可以进行编译了
    return 0;
}
• 通过将Derived1Derived2Base的继承声明为虚继承(virtual public Base)之后,FinalDerived 类中只会有一份Base类的成员。无论通过Derived1还是Derived2的路径,访问的都是同一个 Base类的成员。
特点和注意事项
初始化虚基类:在使用虚继承时,虚基类(如上例中的Base类)只能由最派生的类(如FinalDerived)初始化。
内存布局:虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
设计考虑:虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如
组合或接口)避免菱形继承,那通常是更好的选择。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐