哈喽,各位博友们,博主这几天都没有发博客,确实是在憋大招,今天呢,博主将会讲解c++中的类和对象这一大板块,相信学过c++的博友们都知道这一板块的内容是真的多,那么下面跟着博主一块来学习吧。

1.类和对象

目录

1.类和对象

2.this指针

3.类的默认成员函数

4.构造函数

5.初始化列表构造函数

6.对于构造函数的总结

7.隐式类型转换

8.匿名对象

9.拷贝构造函数

​编辑10.析构函数

11.运算符重载

12.赋值运算符重载

13.取地址运算符重载

14.static成员变量

15.友元

16.内部类

17.结语

如果您可以看到这,我很尊重您,因为我在您身上看到了您对知识的尊重,希望这篇接近1万字的讲解能够对您有帮助,如果您方便的话,可以给博主一个关注和点赞吗?您的支持与认可就是博主前进的动力,谢谢!


        上文中说到了类中的成员函数都是内联函数,那么这个类到底是什么东西呢?先看一段代码

我认为类其实也是一种封装的方式。而类也有以下特点:

1.class为定义类的关键字,A为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省 略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的⽅法或 者成员函数。

2.并且类也有访问权限,public代表可以访问(类外也可以访问)这里一般存储的都是成员函数,而private(类外不可以访问,不过可以通过特殊的方法访问,比如在类中定义一个获取成员变量的函数,在类外调用这个函数即可。还有友元也可以,后面会讲到。)。也体现了他的封装性。

3.为了区分成员变量,一般会在它的前面加上"_"等标识符(因人而异)。

4.第二个图片,由类定义的a就是对象,并且给a开辟了空间,代表类的实例化对象。即类是一个模板或者蓝图,用于描述一类事物(类名)的共同属性(数据)和行为(方法),即成员变量,成员函数。

5.且声明与定义分离的时候,需要指定类域,否则编译器怎么知道去哪里找呢?(一般都会先局部 

找,再去全局找)(指定类域还是用"类名::"去找即可)

2.this指针

接下来咱们看this指针。this指针是什么东西呢?编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。⽐如A类的Init的真实原型为, void Init(A* const this, int a, int b, int c)。

类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_a赋值,this->_a=a;

那么它的作用最重要的还是分辨不同对象 ,看下面一段代码就知道了。

要注意的是c++规定不可以在形参和实参的位置写this指针(编译器会处理),不过函数体里面可以写(函数体里面写不写都无所谓)。

3.类的默认成员函数

        那么一直到这里,都还没有讲到c++类和对象的重点部分,那么从这开始一直到析构函数(不包括类型转换以及匿名对象),博主认为是c++类和对象板块最重要的部分。

默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我 们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前5个,最 后两个取地址重载不重要。

1.构造函数 2.析构函数 3.拷贝构造函数 4.运算符重载 5.赋值运算符重载 6.取地址运算符重载

4.构造函数

        其实构造函数,顶着个“构造”的名字,但其实它的功能并不是取构造一个函数,它的功能是去“初始化”,即对象实例化时初始化对象。

先说一下,内置类型:即语言提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字自己定义的类型。

先说一下构造函数的特点:

1. 函数名与类名相同。

2. 无返回值。(返回值啥都不需要给,也不需要写void)

3. 对象实例化时系统会自动调用对应的构造函数。

4. 构造函数可以重载。

5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义,编译器将不再生成默认构造函数。

6.其实呢,1.无参构造函数、2.全缺省构造函数、3.我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数,但是呢,。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调用时会存在歧义,总结一下就是,不传实参就可以调用的构造就叫默认构造。

7.对于我们不写,编译器默认生成的构造(即默认构造函数是不会出现在代码中的,除非显式调用),对内置类型不做要求,即初不初始化,看编译器。但是对于自定义成员类型,必须得调用这个成员变量自己的构造函数才可以完成初始化。如果编译器没有生成默认构造函数呢?就得用初始化列表来解决。

        那么我们来看上面的代码,构造函数分为三种,无参构造,带参构造,全缺省构造。还要注意的是可以构造函数的,即完成初始化的,其实有三种方式。如上面第二张代码图片。上面涉及到的内容,待会讲解。

        那么关于内置成员类型的默认构造,看一组代码

可以看出对于内置成员类型的默认构造函数,编译器确实是随机初始化的。

5.初始化列表构造函数

        对于构造函数,其实有两种方法,第一种就是上面的第4点,在函数体内初始化成员变量,第二种就是要讲的初始化列表,其实,相较于第一种方法,还是初始化列表更高效。

来看初始化列表的特点:

1.初始化列表不是放在函数体里的,是放在类中构造函数的{}的前面。初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。

2.每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地方(这句话很重要,后面会讲解)

3.C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的 成员使用的。

4.尽量走初始化列表去初始化成员变量,如果这 个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没 有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,注意:C++并没有规定。对于没有 显示在初始化列表初始化的自定义成员类型会调用这个成员类型的默认构造函数,如果没有默认构 造会编译错误。

5.初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。所以说,声明顺序要和初始化顺序保持一致。

6.但其实呢,无论是否显示写初始化列表,每个构造函数都有初始化列表; 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;(所以说,干脆就用初始化列表初始化得了)。

7.对于第二点的:初始化列表可以认为是每个成员变量定义初始化的地方。所以说,有三种变量是必须得走初始化列表的。1.引用成员变量2.const成员变量3.没有默认构造的成员变量。这三个必须走初始化列表,或者未显式初始化,但是有缺省值。(const  int _n;(const成员变量)int &_n;(引用成员变量))。

所以说,走初始化列表的顺序是,先走初始化列表,如果没有显式初始化列表,那么调用成员变量的缺省值,那么如果还是没给缺省值。那么自定义成员类型,调用他们自己的默认构造函数。内置成员类型,初不初始化,看编译器。还要注意一下,三种变量只能走初始化列表。 

6.对于构造函数的总结

c++中构造函数,有函数体内赋值与初始化列表这两种方式,而初始化列表效率更高,这两种方式不建议混用,而且如果这两种方式都没有的话,最后会调用默认构造函数,(默认构造函数只是未显式初始化的类类型成员,而对于没有显式初始化的内置类型成员,给不给它初始化看编译器)。

(精华部分)

7.隐式类型转换

        其实在上面的代码中已经写了隐式类型转换了。

1.即双方的类型不同,而又想让一个值赋值给另外一个对象,那么就用到了隐式类型转换。

2.C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

3.构造函数前⾯加explicit就不再⽀持隐式类型转换。(例如:explicit A(int a1))

4.类类型的对象之间也可以隐式转换,需要相应的构造函数支持

如上图代码,为什么有个临时变量呢?还记得咱们前面文章说过的类型转换,并不是直接转换的,而是先拷贝到临时变量中(临时变量具有常性,需要用const修饰),然后临时变量再拷贝构造(下文会讲到,请耐心往下看)给对象,完成类型的转换。但是,编译器不会管这么多,编译器会直接优化,优化成{4,5,6}为实参,即作为参数传过去。

注:隐式类型转化代码在上图的第二种,后面有注释。

8.匿名对象

1.用类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型对象名(实参)定义出来的 叫有名对象

2. 匿名对象⽣命周期只在当前一行,⼀般临时定义⼀个对象当前用一下即可,就可以定义匿名对象。

上图代码第二行就是创建了一个匿名对象。

来看一下:匿名对象与正常对象的对比:

1. 基本创建与使用对比

匿名对象方式

class MyClass {
public:
    MyClass(int x) : data(x) {
        cout << "构造: " << data << endl;
    }
    ~MyClass() {
        cout << "析构: " << data << endl;
    }
    void show() const {
        cout << "数据: " << data << endl;
    }
    int getData() const { return data; }
private:
    int data;
};

// 匿名对象使用
void demoBasic() {
    cout << "=== 匿名对象 ===" << endl;
    MyClass(100).show();  // 创建后立即使用,当前表达式结束即销毁
    cout << "当前行结束" << endl;
}
/* 输出:
构造: 100
数据: 100
析构: 100
当前行结束
*/

普通对象方式

void demoNormal() {
    cout << "=== 普通对象 ===" << endl;
    MyClass obj(100);  // 创建命名对象
    obj.show();        // 使用对象
    cout << "当前行结束" << endl;
} // obj在这里析构
/* 输出:
构造: 100
数据: 100
当前行结束
析构: 100
*/

2. 函数参数传递对比

匿名对象作为参数

void processByValue(MyClass obj) {
    cout << "处理对象: ";
    obj.show();
}

void processByRef(const MyClass& obj) {
    cout << "引用处理: ";
    obj.show();
}

void demoFunctionParams() {
    cout << "=== 匿名对象参数 ===" << endl;
    processByValue(MyClass(200));    // 匿名对象,可能触发移动构造
    cout << "---" << endl;
    processByRef(MyClass(300));      // 常引用,高效无拷贝
}
/* 输出:
构造: 200
处理对象: 数据: 200
析构: 200
---
构造: 300
引用处理: 数据: 300
析构: 300
*/

普通对象作为参数

void demoNormalParams() {
    cout << "=== 普通对象参数 ===" << endl;
    MyClass obj1(200);
    processByValue(obj1);           // 触发拷贝构造
    cout << "---" << endl;
    MyClass obj2(300);
    processByRef(obj2);             // 无拷贝
}
/* 输出:
构造: 200
拷贝构造: 200      ← 注意这里的拷贝!
处理对象: 数据: 200
析构: 200
---
构造: 300
引用处理: 数据: 300
析构: 300
*/

3. 返回值场景对比

返回匿名对象

MyClass createAnonymous(int x) {
    return MyClass(x);  // 返回匿名对象,可能被RVO优化
}

MyClass createNormal(int x) {
    MyClass obj(x);     // 创建命名对象
    return obj;         // 返回命名对象,可能被NRVO优化
}

void demoReturn() {
    cout << "=== 返回匿名对象 ===" << endl;
    MyClass result1 = createAnonymous(400);
    cout << "---" << endl;
    MyClass result2 = createNormal(500);
}
/* 输出(有RVO优化时):
构造: 400
构造: 500
*/

4. 链式调用对比

匿名对象链式调用

class Calculator {
public:
    Calculator(int val) : value(val) {}
    
    Calculator add(int x) const {
        return Calculator(value + x);  // 返回匿名对象
    }
    
    Calculator multiply(int x) const {
        return Calculator(value * x);  // 返回匿名对象
    }
    
    int getValue() const { return value; }
    
private:
    int value;
};

void demoChainingAnonymous() {
    cout << "=== 匿名对象链式调用 ===" << endl;
    int result = Calculator(10)    // 匿名对象
                 .add(5)          // 返回新匿名对象
                 .multiply(2)     // 继续链式
                 .getValue();     // 获取最终值
    cout << "结果: " << result << endl;  // 输出: 30
}

普通对象链式调用

void demoChainingNormal() {
    cout << "=== 普通对象链式调用 ===" << endl;
    Calculator calc(10);           // 创建命名对象
    Calculator calc2 = calc.add(5); // 中间对象
    Calculator calc3 = calc2.multiply(2); // 另一个中间对象
    int result = calc3.getValue();
    cout << "结果: " << result << endl;  // 输出: 30
}

5. 容器操作对比

匿名对象插入容器

void demoContainerAnonymous() {
    cout << "=== 容器+匿名对象 ===" << endl;
    vector<MyClass> vec;
    
    vec.push_back(MyClass(600));      // 匿名对象,可能触发移动
    vec.emplace_back(700);           // 原地构造,最高效
    
    cout << "容器大小: " << vec.size() << endl;
}

普通对象插入容器

void demoContainerNormal() {
    cout << "=== 容器+普通对象 ===" << endl;
    vector<MyClass> vec;
    
    MyClass obj1(600);
    vec.push_back(obj1);             // 触发拷贝构造
    
    MyClass obj2(700);
    vec.push_back(std::move(obj2));  // 移动语义
    
    cout << "容器大小: " << vec.size() << endl;
}

6. 运算符重载对比

匿名对象在运算符中

class Complex {
public:
    Complex(double r, double i) : real(r), imag(i) {}
    
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag); // 返回匿名对象
    }
    
    void display() const {
        cout << real << " + " << imag << "i" << endl;
    }
    
private:
    double real, imag;
};

void demoOperatorAnonymous() {
    cout << "=== 运算符+匿名对象 ===" << endl;
    Complex c1(2, 3), c2(4, 5);
    
    // 使用匿名对象进行复杂表达式
    Complex result = c1 + c2 + Complex(1, 1);
    result.display();
}

普通对象在运算符中

void demoOperatorNormal() {
    cout << "=== 运算符+普通对象 ===" << endl;
    Complex c1(2, 3), c2(4, 5);
    Complex temp(1, 1);
    
    // 使用命名对象
    Complex intermediate = c1 + c2;
    Complex result = intermediate + temp;
    result.display();
}

7. 工厂模式对比

匿名对象工厂

class Connection {
public:
    static Connection create() {
        return Connection();  // 返回匿名对象
    }
    
    void connect() { cout << "连接建立" << endl; }
    
private:
    Connection() = default;
};

void demoFactoryAnonymous() {
    cout << "=== 工厂+匿名对象 ===" << endl;
    auto conn = Connection::create();  // 直接使用匿名对象
    conn.connect();
}

普通对象工厂

class Connection2 {
public:
    static Connection2* create() {
        return new Connection2();  // 返回指针,需要手动管理
    }
    
    void connect() { cout << "连接建立" << endl; }
    
private:
    Connection2() = default;
};

void demoFactoryNormal() {
    cout << "=== 工厂+普通对象 ===" << endl;
    Connection2* conn = Connection2::create();  // 需要手动管理内存
    conn->connect();
    delete conn;
}

8. 性能对比测试

class Heavy {
public:
    Heavy() { 
        data = new int[1000]; 
        cout << "分配内存" << endl;
    }
    Heavy(const Heavy& other) {
        data = new int[1000];
        cout << "拷贝构造" << endl;
    }
    Heavy(Heavy&& other) noexcept {
        data = other.data;
        other.data = nullptr;
        cout << "移动构造" << endl;
    }
    ~Heavy() { 
        delete[] data; 
        cout << "释放内存" << endl;
    }
    
private:
    int* data;
};

void demoPerformance() {
    cout << "=== 性能对比 ===" << endl;
    
    cout << "匿名对象方式:" << endl;
    {
        Heavy&& h = Heavy();  // 直接使用匿名对象
    }
    
    cout << "普通对象方式:" << endl;
    {
        Heavy h;              // 创建命名对象
    }
    
    cout << "函数参数匿名:" << endl;
    {
        auto func = [](const Heavy& h) {};
        func(Heavy());        // 匿名对象参数
    }
    
    cout << "函数参数普通:" << endl;
    {
        auto func = [](const Heavy& h) {};
        Heavy h;
        func(h);              // 命名对象参数
    }
}

9. 实际应用场景总结

适合使用匿名对象的场景

// 1. 一次性使用的临时对象
std::sort(vec.begin(), vec.end(), std::greater<int>());

// 2. 链式方法调用
std::string("hello").append(" world").substr(0, 5);

// 3. 函数式编程风格
auto result = std::accumulate(vec.begin(), vec.end(), 0, 
                             [](int a, int b) { return a + b; });

// 4. 测试和调试
EXPECT_EQ(std::string("test").length(), 4);

// 5. 表达式中的临时计算
double area = Circle(5.0).getArea();

适合使用普通对象的场景

// 1. 需要重复使用的对象
MyClass obj;
obj.method1();
obj.method2();
obj.method3();

// 2. 需要明确生命周期的对象
{
    File file("data.txt");  // 需要在整个作用域内存在
    file.read();
    file.process();
} // 文件在这里关闭

// 3. 需要共享所有权的对象
auto shared_obj = std::make_shared<MyClass>();

// 4. 需要维护状态的对象
Logger logger;
logger.setLevel(LogLevel::DEBUG);
logger.log("message");

10. 综合对比表格

特性匿名对象普通对象
生命周期当前表达式结束作用域结束
内存管理自动管理自动管理
性能通常更好(RVO优化)可能有多余拷贝
代码简洁性中等
可读性需要熟悉语法更直观
调试难度较难调试容易调试
适用场景临时计算、链式调用状态维护、复杂逻辑

9.拷贝构造函数

        上文中咱们也用到了拷贝构造函数,那么拷贝构造函数,有什么用呢?拷贝构造函数,是一种特殊的构造函数,用于同类型的另一对象来初始化对象。注意:拷贝构造用于⼀个对象拷贝初始化给另一个要创建的对象。即类名 要创建的对象(一个对象)。

1.拷贝构造函数是构造函数的⼀个重载。

2. 拷贝构造函数的第⼀个参数必须是类类型对象的引用,使用传值⽅式编译器直接报错,因为语法逻 辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引 用,后⾯的参数必须有缺省值。(下文会讲解)

3. C++规定⾃定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成。

4. 若未显式定义拷贝构造,编译器会⽣成⾃动⽣成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。

5.传值返回会产生⼀个临时对象调用1、拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没 有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用 引用返回是有问题的,这时的引通相当于⼀个野引⽤,类似⼀个野指针⼀样。传引用返回可以减少 拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。(下文也会讲解)。

对于第二点的逻辑上的无穷递归,咱们知道,形参是实参的一份拷贝对吧(传值传参),那么既然要拷贝,是不是得调用拷贝构造?那么调用了拷贝构造函数,是不是得传值传参?那么传值又得拷贝构造,所以引发了无限递归。用表就是这样的:

传值传参->拷贝构造->传值传参->拷贝构造->传值传参..............引发无限递归。

对于第五点的传值返回,和传引用返回,先看一组代码以及报错信息。?不知道怎么回事,图片显示不出来了,剩下的图片博主就放到下一篇了,记得去查收。

//Date& Func2()
//{
//    Date tmp(2024, 7, 5);
//    tmp.Print();
//    return tmp;
//}

/    // Func2返回了⼀个局部对象tmp的引⽤作为返回值
//
//        // Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
//
//   // Date ret = Func2();
//    //ret.Print();

传引用返回:你必须得保证你要返回的那个变量,在函数栈帧回收后不会被销毁,用大白话来说就是你得保证你要返回的那个变量不是定义在这个函数里的,因为只有这样才可以保证函数栈帧被回收后,这个变量不会被销毁。不过这个可以减少拷贝。

传值返回;这个就是用于你如果不想改变一个变量b的值,那么你就再定义一个变量a,然后把变量b的值赋值给变量a,最后返回变量a,但是很明显这种方式是不是多了拷贝。

10.析构函数

析构函数与构造函数功能相反,,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对 象中资源的清理释放⼯作。

析构函数的特点:

1. 析构函数名是在类名前加上字符~

2. ⽆参数⽆返回值。(这⾥跟构造类似,也不需要加void)

3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。

5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会 调⽤他的析构函数。

6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数

7.我们还要注意的是析构函数的顺序:⼀个局部域的多个对象,C++规定后定义的先析构。

11.运算符重载

在c++中,对于内置类型的打印,咱们直接cout<<.....<<endl;即可,但是对于自定义类型这种复杂类型的打印呢?这时候就用到了运算符重载,顾名思义,重新定义一个运算符,然它实现更复杂的功能。

1.运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其 他函数⼀样,它也具有其返回类型和参数列表以及函数体。

 2.重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元 运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

3.如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个(但要不是成员函数,就不需要再参数比运算对象少一个了,下文会讲)。

class Complex {
private:
    double real, imag;
public:
    Complex(double r, double i) : real(r), imag(i) {}
    
    // 运算符重载作为成员函数
    Complex operator+(const Complex& other) const {
        // 这里的 this 指向第一个运算对象
        return Complex(this->real + other.real, this->imag + other.imag);
    }
};

// 使用
Complex a(1, 2), b(3, 4);
Complex c = a + b;  // 等价于 a.operator+(b)

分析:

1.a + b 中,a 通过 this 指针隐式传递

2.b 作为显式参数 other 传递

3.参数比运算对象少1个:虽然有两个运算对象(a和b),但函数参数只有1个

  1. this 指针的作用

    a + b  // 成员函数中相当于:
    // this = &a, other = b
  2. 为什么参数少一个

    • 运算对象:a, b(2个)

    • 函数参数:只有b(1个),a通过this传递

  3. 适用情况

    • 一元运算符:成员函数需要0个参数

    • 二元运算符:成员函数需要1个参数

4.        .*        ::        sizeof            ?:        . 注意以上5个运算符不能重载。

5.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

这里看一下第5点的代码:

//++d1

Date& Date::operator++()

{ *this += 1; return *this; }

// d1++

Date Date::operator++(int)

{ Date tmp(*this); *this += 1; return tmp;}

6.重载<<和>>时,需要重载为全局函数,因为重载为成员函数 ,this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象,调用时就变成了对象<<out,不符合“cout<<”的使用习惯。所以重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

// ❌ 错误:如果作为成员函数
class Date {
public:
    ostream& operator<<(ostream& out) {  // this占据第一个参数位置
        out << this->_year;              // 使用时:date << cout (反人类!)
        return out;
    }
};

// ✅ 正确:作为全局函数
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "年" << d._month << "月" << d._day << "日";
    return out;
}
// 使用时:cout << date (符合习惯)
//关键:cout << date 中,cout 是第一个运算对象,必须作为第一个参数。

友元声明+外部定义的完整流程:

.h头文件:

class Date {
private:
    int _year, _month, _day;
public:
    Date(int y, int m, int d) : _year(y), _month(m), _day(d) {}
    
    // 1. 友元函数声明 - 让全局函数能访问私有成员
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
};

// 2. 函数声明 - 告诉编译器这个函数存在
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

通过这里咱们可以看到,友元函数的作用是什么呢?就是为了让全局函数能够访问私有成员!!!(具体看下面的友元讲解)

.cpp源文件:

// 3. 函数定义 - 实现具体功能
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "年" << d._month << "月" << d._day << "日";
    return out;  // 注意:这里应该是 return out,不是 return 0
}

istream& operator>>(istream& in, Date& d) {
    in >> d._year >> d._month >> d._day;
    return in;
}

1.这里返回值是ostream&是输入输出流对象,用传引用返回的目的是,为了支持链式访问,相当于调用这次函数后,这个out改变了,直接返回改变的这个,out再次调用,形成链式访问(而这里总共调用了6次函数,所以为了让它形成链式访问(out << d._year << " 年 " << d._month << " ⽉ " << d._day << " ⽇ " << endl),就用传引用返回即可。)

// 链式调用原理:
cout << date1 << " " << date2 << endl;
// 等价于:
operator<<(operator<<(operator<<(cout, date1), " "), date2);

// 每次调用都返回同一个ostream对象,支持连续调用

2.这里,ostream的函数中第二个参数是const 修饰,是因为,你输出,肯定不想让自己输出的值发生改变,所以加const,但是第二个istream不需要加const,是因为你输入,肯定会改变,所以不需要加const。

// 输出不加const:❌ 编译错误,无法修改
// 输入加const:❌ 编译错误,无法修改对象
ostream& operator<<(ostream& out, const Date& d);  // ✅ 输出只读
istream& operator>>(istream& in, Date& d);         // ✅ 输入可写

3.为什么类中已经友元声明了一遍,还要在外面重载一遍?注意,声明是声明,定义是定义,别混淆了。

// 声明:告诉编译器函数签名(在.h中)
friend ostream& operator<<(ostream& out, const Date& d);

// 定义:实现函数功能(在.cpp中)  
ostream& operator<<(ostream& out, const Date& d) {
    // 具体实现
}

当然了,除了这样,你还可以友元函数声明与定义都写在同一个类里面,这样也是可以的。

12.赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟 拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。(注意跟拷贝构造区分开),拷贝构造函数是一个对象存在,而另外一个要创建的对象不存在。

赋值运算符的特点:

1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引⽤,否则会传值传参会有拷⻉

2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋 值场景。

3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷 ⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。

//拷贝构造
//    Date(const Date& d)
//    {
//        cout << " Date(const Date& d)" << endl;
//        _year = d._year;
//        _month = d._month;
//        _day = d._day;
//    }
//    // 传引⽤返回减少拷⻉
//
//        // d1 = d2;
//        Date& operator=(const Date& d)
//    {
//        // 不要检查⾃⼰给⾃⼰赋值的情况
//
//            if (this != &d)
//            {
//                _year = d._year;
//                _month = d._month;
//                _day = d._day;
//            }
//        // d1 = d2表达式的返回对象应该为d1,也就是* this
//            return *this;

int main()
//{
//    Date d1(2024, 7, 5);
//    Date d2(d1);//拷贝构造
//    Date d3(2024, 7, 6);
//    d1 = d3;
//    d1.Print();
//    d3.Print();
//    //需要注意这⾥是拷⻉构造,不是赋值重载
//    //请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
//    //⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
//
//    Date d4 = d1;//拷贝构造

//return 0;

//}

13.取地址运算符重载

cosnt成员函数

1.将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 ⾯。

2.const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。 const 修饰Date类的Print成员函数,Print隐含的this指针由Date*const this 变为 const Date*const this.

3.即是保证这个函数中的this指针指向的变量不被修改。

请看下面一段代码:

// void Print(const Date* const this) const

void Print() const

取地址运算符重载:

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

14.static成员变量

咱们来看几个例子就清楚了:

class Example {
private:
    static int s_count;    // 静态成员
    int instance_data;     // 实例成员
public:
    static void staticFunc() {
        s_count = 10;      // ✅ 静态函数可以访问静态成员
         //instance_data = 20; // ❌ 不能访问实例成员
        cout << s_count << endl;
    }

    friend void friendFunc(Example& e);
       
    
};
int Example::s_count = 10;//这个初始化必须得有,否则就会报错
 void friendFunc(Example& e) {
    e.s_count = 30;        // ✅ 友元可以访问静态成员  
    e.instance_data = 40;  // ✅ 友元也可以访问实例成员
    cout << e.s_count << " " << e.instance_data << endl;
}
int main()
{
    Example obj;

    // 静态成员函数调用
    Example::staticFunc();  // 通过类名调用,不需要创建对象就能调用
    obj.staticFunc();       // 也可以通过对象调用(但不推荐)

    // 友元函数调用
    friendFunc(obj);        // 像普通函数一样调用
    return 0;
}
class MemoryExample {
public:
    static int s_var;     // 在全局数据区,所有对象共享
    int instance_var;     // 在每个对象中独立存在
};

int MemoryExample::s_var = 0;  // 静态成员需要在类外定义

int main()
{
    // 使用:
    MemoryExample obj1, obj2;
    obj1.s_var = 100;        // 所有对象共享同一个s_var
    cout << obj2.s_var;      // 输出100
    return 0;
}

1.⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。(你想想,它都属于全部对象了,能单独初始化嘛?)

2.静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区

所以他的生命周期与全局变量一样。

3.⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针(只可访问静态成员变量,不可以访问成员变量)

4. 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针(友元函数也没有this指针)

5. 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

总结下来就是:友元函数可以访问类内的私有静态成员变量,然后静态成员函数可以访问静态成员变量,但是不可以访问非静态成员变量。非静态成员函数既可以访问静态成员变量,又可以访问非静态成员变量。

6. 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。

7. 静态成员也是类的成员,受public、protected、private访问限定符的限制。

8.静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表

这个变量有什么特点呢?就是整个类中的对象都可以访问(一般通过this指针,一个对象都有他自己的成员变量,而通过static修饰,使得类中的所有的对象共享一个成员变量。

还要注意的是局部静态变量只有在第一次执行他的时候才会构造(初始化),并且只会构造一次,并且析构只会在main函数结束后才会析构,因为其生命周期是整个程序。且局部静态变量析构后,才是全局变量的析构。

静态成员是"内部的特权员工",友元是"外部的关系户"。(所以静态成员可以在所有对象内走动,而友元是在你想访问别人的私有成员变量时使用)。

非静态成员函数:

class MemoryExample {
public:
    static int s_data;     // 在全局数据区,所有对象共享
    int i_data;            // 在堆/栈区,每个对象独立

     void nonStaticFunc() {
        // 访问静态成员:通过类名或直接访问
        s_data = 10;           // 直接访问
        MemoryExample::s_data = 20;  // 通过类名访问
        cout << s_data << endl;

        // 访问实例成员:通过this指针
        i_data = 30;           // 等价于 this->i_data = 30
    }
};

int MemoryExample::s_data = 0;

// 内存布局:
// s_data: 全局唯一,所有MemoryExample对象共享
// 每个MemoryExample对象有自己的i_data
int main()
{
    MemoryExample obj;
    obj.nonStaticFunc();
    return 0;
}

静态成员函数:

class MemoryExample {
public:
    static int s_data;     // 在全局数据区,所有对象共享
    int i_data;            // 在堆/栈区,每个对象独立

    static void nonStaticFunc() {
        // 访问静态成员:通过类名或直接访问
        s_data = 10;           // 直接访问
        MemoryExample::s_data = 20;  // 通过类名访问
        cout << s_data << endl;

        // 访问实例成员:通过this指针
        //i_data = 30;           // 等价于 this->i_data = 30,会报错!
    }
};

int MemoryExample::s_data = 0;

// 内存布局:
// s_data: 全局唯一,所有MemoryExample对象共享
// 每个MemoryExample对象有自己的i_data
int main()
{
    MemoryExample::nonStaticFunc();
    return 0;
}

15.友元

前文中咱们提到了类外访问private中的成员变量,除了在类中构造一个可以访问成员变量的成员函数,并在类外调用函数,还有一种方式就是友元。

友元函数的主要作用就是让非成员函数能够访问类的私有成员。

class MyClass {
private:
    int private_data;
public:
    friend void friendFunction(MyClass& obj);  // 友元声明
};

void friendFunction(MyClass& obj) {
    obj.private_data = 100;  // 可以访问私有成员
    std::cout << obj.private_data;
}
int main()
{
    MyClass my;
    friendFunction(my);//友元函数直接这样调用即可。就跟正常的函数没什么区别
    return 0;
}

1.假如你现在想让A类访问B类的私有成员变量,那么这种情况下:A类是B类的友元,B类把A类设置为友元。

想访问谁的私有成员变量,就让另外一个成为友元(就在"谁的"里面添加友元类)

需要注意的是,你访问的是B类的私有成员变量,所以说你的accessB的参数,一定得是B来接收的。

看谁是友元,友元就是走后门的,谁是友元,谁就可以访问另外一个类的私有成员变量

class A;  // 前向声明

class B {
private:
    int b_secret = 100;

public:
    // 在B类内部声明A为友元
    friend class A;  // 关键语句
};

class A {
public:
    void accessB(B& b_obj) {
        cout << "A访问B的私有成员: " << b_obj.b_secret << endl;  // ✅
        b_obj.b_secret = 200;  // ✅ 甚至可以修改
    }
};
int main()
{
    A obj_a;
    B obj_b;

    obj_a.accessB(obj_b);
    return 0;
}

2.单向性:

class A {
    friend class B;  // B可以访问A的私有,但A不能访问B的私有
};

class B {
private:
    int b_secret;  // A无法访问这个
};

3.不可传递性:

class A {
    friend class B;
};

class B {
    friend class C;  // 但C不能访问A的私有成员!
};

class C {
    // 不能访问A的私有成员
};

4.需要前向声明:

class B;  // 前向声明

class A {
    friend void B::someFunction(A& a);  // 需要知道B的存在
};

友元的特点:

1.友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类 声明的前面加friend,并且把友元声明放到⼀个类的里面。

class Date {
private:
    int _year, _month, _day;
public:
    // 友元声明:让全局函数能访问私有成员
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
};

// 没有友元声明的话,这里无法访问 _year, _month, _day
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "年" << d._month << "月" << d._day << "日";  // ✅ 可以访问私有成员
    return out;
}

 2.外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数

 3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。

4 ⼀个函数可以是多个类的友元函数。

class Student;
class Teacher;

class Student {
private:
    int score;
    friend void printRelation(const Student& s, const Teacher& t);
};

class Teacher {
private:  
    string evaluation;
    friend void printRelation(const Student& s, const Teacher& t);
};

// 这个函数需要访问两个类的私有成员
void printRelation(const Student& s, const Teacher& t) {
    cout << "成绩:" << s.score << ", 评价:" << t.evaluation << endl;
}

 5.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

6.友元类的关系是单向的,不具有交换性,比如A类是B类的友元(A是B的友元,则A可以访问B中的私有成员变量),但是B类不是A类的友元。

7. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。

 8.有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

16.内部类

1. 如果⼀个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在 全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

2. 内部类默认是外部类的友元类。//这个是正确的

3. 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

4.内部类默认可以访问外部类的所有成员,但是外部类要是想访问内部类的成员,就需要内部类声明外部类为友元。因为外部类想走关系。

class Outer {
private:
    int private_data = 100;

public:
    class Inner {
    private:
        int a = 1000;
    public:
        friend class Outer;
        
        void tryAccess(Outer& obj) {
            std::cout << obj.private_data;  
            // 内部类默认可以访问外部类的所有成员
            //但是外部类要是想访问内部类的成员,就需要内部类声明外部类为友元。因为外部类想走关系,
            //所以肯定是内部类将外部类声明为友元

            //就是外部类是内部类的友元,这个得是内部类将外部类声明为友元。
            //A类想访问B类的私有成员变量,就是B类得把A类设置为友元,A类是B类的友元。
        }

    };
    void tryAccess1(const Outer::Inner& obj)
    {
        std::cout << obj.a;
    }

};

int main()
{

    Outer o;
    Outer::Inner i;
    i.tryAccess(o);
    o.tryAccess1(i);
    return 0;
}

17.结语

激动地心,颤抖地手,重要把这一个重要的板块写完了,算下来,敲代码加上写文章的时间也将近10小时了,还不算上我理解的时间,为的就是为博友们提供更好的理解。


以上就是我关于类和对象的看法与理解

如果您可以看到这,我很尊重您,因为我在您身上看到了您对知识的尊重,希望这篇讲解文章能够对您有帮助,如果您方便的话,可以给博主一个关注和点赞吗?您的支持与认可就是博主前进的动力,谢谢!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐