【C++】类与对象(下)

🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: 小米里的大麦——C++专栏_CSDN博客
🎁 代码托管: 小米里的大麦的Gitee仓库
⚙️ 操作环境: Visual Studio 2022

在这里插入图片描述

文章目录

    • 1. 再谈构造函数
      • 1.1 构造函数体赋值
      • 1.2 初始化列表
      • 1.3 explicit 关键字
    • 2. static成员
      • 2.1 概念
        • 2.2 特性
      • 例子:
      • 传道解惑:
        • Q1:静态成员函数可以调用非静态成员函数吗?
        • Q2: 非静态成员函数可以调用类的静态成员函数吗?
    • 3. 友元(Friend)
      • 3.1 友元函数
      • 传道解惑
        • Q1:为什么使用友元函数?
          • 示例:重载`operator<<`(输出流)为友元函数
      • 3.2 友元类
        • 友元类的特性:
        • 示例:友元类
      • SO:
    • 4. 内部类(Nested Class)
      • 4.1 概念:
        • 关键点:
      • 4.2 内部类的特性
      • 4.3 内部类的分类
      • 示例:内部类的使用
        • 1. 普通的内部类
        • 2. 静态内部类
        • 3. 普通内部类和静态内部类的区别:
      • SO:
    • 5. 匿名对象(Anonymous Object)
      • 5.1 匿名对象的定义和创建
      • 5.2 匿名对象的生命周期
      • 5.3 匿名对象的使用示例
        • 示例 1:作为函数返回值的匿名对象
        • 示例 2:作为临时对象
        • 示例 3:通过类型转换创建匿名对象
      • 5.4 匿名对象的应用场景
      • 5.5 注意事项
    • 传道解惑
      • Q1:为什么匿名对象加`const`可以延长生命周期?
        • 具体解释:
      • 例子:匿名对象与 `const` 引用
      • 重要说明:
      • Q2:匿名对象 VS 有名对象
      • 总结
    • 6. 再次理解类和对象
      • 1. 类是对事物的抽象
      • 2. 对象是类的实例化
      • 3. 通过类创建对象
      • 4. 类和对象的关系
      • 5. 现实中的例子:洗衣机类
      • SO:
    • 共勉

1. 再谈构造函数

在面向对象编程中,构造函数是一种特殊的成员函数,它在对象创建时自动调用,负责初始化对象的成员变量(创建对象时赋初值),确保对象在创建时有一个有效的状态。接下来,我们将详细讲解关于构造函数的几个重要概念。

1.1 构造函数体赋值

当我们创建一个对象时,构造函数会被自动调用,用来给对象的各个成员变量提供一个初始值。例如:

class MyClass
{
public:int a;MyClass(){a = 10;  // 这里是构造函数体中的赋值操作}
};

在这个例子中,构造函数的作用是将a赋值为10。然而,这里要注意,构造函数体中的赋值操作和初始化是有区别的。构造函数体中对成员变量的赋值只能算是给成员变量“赋初值”,而不是“初始化”。因为初始化是指给成员变量设置一个初始值,而且初始化只能发生一次,而赋值操作可以发生多次。

再例如:

class A
{int _x;
public:A(int x){_x = x; // 赋值操作:_x 在此之前已经默认初始化(如果有的话),然后被赋值为 x。}
};上述代码中,`_x = x;`是赋值,而非初始化,因为初始化只能通过构造函数外部的初始化列表来完成。在构造函数体中对成员变量的赋值操作,实际上是先通过默认初始化(如果有的话),然后通过赋值操作覆盖初始值。这与初始化列表直接初始化成员变量有本质区别。

换句话说,初始化只能在构造函数中通过初始化列表来进行一次,而构造函数体中的赋值可以反复进行

所以,赋值与初始化的区别在于:

  • 初始化:是在对象创建时为成员变量设置初始值的过程,每个成员变量只能被初始化一次,通常在构造函数的初始化列表中进行(只能进行一次)。
  • 赋值:是在对象创建后对成员变量进行值的修改,可以发生多次,可以在构造函数体中,也可以在对象的生命周期中的任何时候进行(可以重复进行)。

1.2 初始化列表

初始化列表是构造函数中的一种写法(直接初始化类的成员变量的一种方式),允许我们在构造函数调用之前就给成员变量提供初始值。语法是在构造函数的括号后加冒号和成员变量列表,其基本格式是:

MyClass() : a(10)
{// 构造函数体
}这里的:`a(10)`就是初始化列表,它会在构造函数体执行之前初始化成员变量`a`为10

再或者:

class A
{int _x;int _y;int _val;
public:A(int x): _x(x), _y(0), _val(0){} // 初始化列表
};

注意事项

  1. 每个成员变量在初始化列表中只能出现一次。初始化列表的目的是对成员进行一次初始化。
  2. 对于某些特殊类型的成员变量,必须使用初始化列表来进行初始化。例如:
    • 引用成员变量:引用一旦初始化就不可更改,只能在初始化列表中赋值。
    • const成员变量:常量变量(const变量)在对象创建时必须初始化,且不可更改;必须在声明时初始化,不能通过构造函数体赋值。
    • 自定义类型成员/没有默认构造函数的自定义类型:自定义类型变量如果没有默认构造函数,只能通过初始化列表初始化。
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  4. 初始化列表的顺序:初始化列表中的成员变量的初始化顺序和它们在类中声明的顺序一致,而不是按照初始化列表中出现的顺序(编译器会按照成员变量在类中声明的顺序进行内存布局,因此初始化顺序也必须与之匹配)。

推荐使用初始化列表的原因: 对于自定义类型,编译器会自动调用其构造函数完成初始化,不使用初始化列表而采用先默认构造再赋值可能导致性能浪费。

类的成员变量初始化顺序 取决于 成员变量的声明顺序,而不是初始化列表的顺序:

#include <iostream>
using namespace std;class MyClass
{
public:// 此处先声明 a, 再声明 bint a;const int b;MyClass(int x) : b(20), a(x) // 初始化列表:b 在 a 之前初始化(就会导致一些奇怪的错误:未定义行为(UB)){// b 在 a 之后声明,所以 a 会先被初始化,b 才会初始化!// 如果 b 的初始化依赖 a,就会导致意外的错误或 UB!cout << "a = " << a << ", b = " << b << endl;}
};int main()
{MyClass obj(10); // 输出:a = 10, b = 20return 0;
}

1.3 explicit 关键字

如果构造函数接收单一参数,它可能会被编译器用于隐式类型转换。而explicit关键字则用来标记构造函数,防止它参与隐式类型转换。 隐式类型转换可能导致临时对象的创建和销毁,增加不必要的性能开销,或者在代码中引入难以察觉的错误。

在C++中,构造函数不仅可以用来构造和初始化对象,还可以在特定条件下用作类型转换的“隐式转换构造函数”。例如:

class MyClass
{
public:MyClass(int x){// 构造函数}
};// error: conversion from 'int' to non-scalar type 'MyClass' requested
MyClass obj = 10;  // 编译器会用构造函数将10转换为MyClass对象在上述代码中,`MyClass(int x)`构造函数是一个单参构造函数,可以将`int`类型的10隐式转换为`MyClass`类型的对象。
为防止这种类型转换,我们可以在构造函数前加上`explicit`关键字,这样就禁止了隐式转换:class MyClass
{
public:explicit MyClass(int x){// 构造函数}
};MyClass obj = 10;  // 错误:不能隐式转换
class A
{
public:A(int x) {} // 单参构造函数
};A obj = 10; // 隐式调用构造函数**explicit的作用**: 使用`explicit`关键字禁止隐式类型转换:class A
{
public:explicit A(int x) {}
};A obj = 10; // 错误,禁止隐式转换
A obj2(10); // 正确
#include <iostream>
using namespace std;class MyClass
{
public:MyClass(int x) {}
};class MyExplicitClass
{
public:explicit MyExplicitClass(int x) {}
};int main()
{MyClass obj1 = 10;  // 隐式类型转换MyExplicitClass obj2 = 10;  // 错误:禁止隐式类型转换MyExplicitClass obj3(10);  // 显式类型转换cout << "obj1 created successfully." << endl;// cout << "obj2 created successfully." << endl;  // 编译错误cout << "obj3 created successfully." << endl;return 0;
}

使用explicit关键字后,只有明确调用构造函数时才能进行类型转换,不能再进行隐式转换。所以,构造函数不仅仅负责创建对象,它还通过初始化列表给对象成员变量赋初值。通过explicit关键字,我们可以禁止单参构造函数的隐式类型转换,确保代码的类型安全。


2. static成员

2.1 概念

在C++中,类成员可以被声明为static,这样的成员称为静态成员。静态成员包括静态成员变量和静态成员函数。

  • 静态成员变量:使用static修饰的类成员变量,所有对象共享这个变量,不属于某个特定的对象。静态成员变量必须在类外进行初始化(被修饰的变量属于类本身,而不是某个对象)。
  • 静态成员函数:使用static修饰的成员函数,它是属于类的,而不是类的某个对象(修饰的函数属于类本身,可以直接通过类名调用)。
2.2 特性
  1. 静态成员的共享性: 静态成员变量为所有对象共享,修改其中一个对象的静态成员变量值,会影响其他对象。

    • 静态成员变量和静态成员函数是属于类本身的,而不是类的某个特定对象。
    • 所有类对象共享静态成员,这意味着静态成员的值对所有对象是相同的。
    • 静态成员并不存储在每个对象中,而是存放在一个全局的静态区。

    例如:

    class A
    {static int _count;
    public:static void Increment() { _count++; }static int GetCount() { return _count; }
    };int A::_count = 0;//  静态变量必须在类外定义/初始化int main()
    {A::Increment();cout << A::GetCount() << endl; // 输出1
    }
    
  2. 静态成员变量的初始化

    • 静态成员变量在类外进行定义和初始化,而在类内部仅声明。

    • 例如:

      class MyClass
      {
      public:static int count;  // 静态成员变量声明
      };int MyClass::count = 0;  // 静态成员变量初始化
      
  3. 访问静态成员

    • 可以通过类名直接访问静态成员:类名::静态成员
    • 也可以通过对象访问静态成员:对象.静态成员。但是,通常推荐通过类名来访问,保持代码的清晰和一致性。
  4. 静态成员函数没有this指针

    • 静态成员函数与类的对象无关,静态成员函数属于类本身,而不是某个对象,因此它没有隐藏的this指针。
    • 由于没有this指针,静态成员函数无法访问类的非静态成员变量或非静态成员函数(只能访问静态变量,不能访问非静态成员)。
    • 非静态成员属于对象,而静态成员函数与对象无关,因此无法通过this指针访问非静态成员。
  5. 访问权限

    • 静态成员仍然受类的访问控制(publicprotectedprivate)的限制。

例子:

假设我们要实现一个类,统计类的对象创建次数。

#include <iostream>
using namespace std;class MyClass
{
public:static int count; 		// 静态成员变量,属于类本身MyClass(){count++; 			// 每创建一个对象,静态变量 count 加 1}static int getCount(){return count; 		// 静态成员函数,返回静态变量的值}
};int MyClass::count = 0; 	// 类外初始化静态成员变量int main()
{MyClass obj1, obj2, obj3;cout << "创建的对象数:" << MyClass::getCount() << endl; // 输出:创建的对象数:3return 0;
}

静态成员函数和非静态成员函数的区别:

#include <iostream>
using namespace std;class MyClass
{
public:static int staticVar;int nonStaticVar;static void staticFunction(){cout << "static 函数,staticVar =" << staticVar << endl;// cout << "非静态 var =" << nonStaticVar << endl;  // 编译错误}void nonStaticFunction(){cout << "非静态函数,nonStaticVar =" << nonStaticVar << endl;cout << "静态 var =" << staticVar << endl;}
};int MyClass::staticVar = 10;int main()
{MyClass::staticFunction();  // 静态函数调用MyClass obj;obj.nonStaticFunction();  // 非静态函数调用return 0;
}

传道解惑:

Q1:静态成员函数可以调用非静态成员函数吗?

不可以。静态成员函数没有this指针,它与对象无关。因此,它无法访问类的非静态成员变量和非静态成员函数。如果静态成员函数试图访问非静态成员函数或成员变量,会导致编译错误。

例如:

class MyClass
{
public:void nonStaticFunction(){cout << "这是一个非静态函数。" << endl;}static void staticFunction(){// 不能访问非静态成员函数// nonStaticFunction();  // 错误:静态成员函数不能访问非静态成员函数}
};
Q2: 非静态成员函数可以调用类的静态成员函数吗?

可以。非静态成员函数属于某个对象,可以访问类的静态成员变量和静态成员函数。非静态成员函数是通过对象的this指针来访问类的成员的,所以它可以直接访问静态成员。

例如:

class MyClass
{
public:static int staticVar;int nonStaticVar;void nonStaticFunction(){// 可以访问静态成员函数staticFunction();cout << "静态变量:" << staticVar << endl;}static void staticFunction(){cout << "这是一个静态函数。" << endl;}
};int MyClass::staticVar = 10;int main()
{MyClass obj;obj.nonStaticFunction();  // 调用非静态成员函数,内部可以访问静态成员函数return 0;
}

输出:

这是一个静态函数。
静态变量:10

3. 友元(Friend)

友元是一种打破封装的方式,它允许特定的函数或类访问类的私有成员和保护成员。通常情况下,类的私有成员是只能通过类的成员函数来访问的,但友元提供了一个特例,使得外部函数或类可以访问这些私有成员。尽管友元能提供便利,但它也会增加类之间的耦合度,破坏封装性,因此在设计时应当小心使用。

友元可以分为两种类型:友元函数友元类

3.1 友元函数

  • 友元函数的定义: 友元函数是定义在类外的普通函数,但可以访问类的私有成员和保护成员。它并不是类的成员函数,因此没有this指针。虽然它是类外的函数,但为了让它可以访问类的私有成员,必须在类内声明为友元函数,使用friend关键字。

  • 友元函数的特性:

    • 声明位置:可以在类定义的任何地方声明,只要在类内部用friend关键字。
    • 访问权限:友元函数可以访问类的私有和保护成员,但它并不属于类的成员函数。
    • 不属于类的成员函数:友元函数的调用和普通函数一样,不通过对象调用,没有this指针。
    • 不能用const修饰:友元函数不能被声明为const,因为它可能会修改类的私有成员。

    例如:

    #include <iostream>
    using namespace std;class A;  // 前向声明类 Aclass B
    {int _y;
    public:B(int y) : _y(y) {}friend class A;  // 友元类:A 类可以访问 B 的私有成员
    };class A
    {int _x;
    public:A(int x) : _x(x) {}friend void Print(const A& a);  // 友元函数声明// 友元类 A 可以访问 B 的私有成员void Print(const B& b){cout << "Friend class: " << b._y << endl;  // 访问 B 的私有成员}
    };void Print(const A& a)
    {cout << "Friend function: " << a._x << endl;  // 访问 A 的私有成员
    }int main()
    {A a(10);Print(a);  // 调用友元函数B b(20);a.Print(b);  // 调用友元类中的成员函数return 0;
    }
    
  • 友元函数的使用场景

    • 当需要定义一个全局函数,并且需要访问类的私有数据时。
    • 常用于重载操作符<<>>

传道解惑

Q1:为什么使用友元函数?

友元函数常用于那些不能作为成员函数实现的操作。例如,重载输入输出运算符<<>>时,coutcin对象是流对象,需要通过全局函数进行操作,而不能作为类的成员函数。

示例:重载operator<<(输出流)为友元函数

假设我们有一个Box类,想要重载<<运算符来输出Box的内容,cout需要是左操作数,因此<<不能作为成员函数。

#include <iostream>
using namespace std;class Box
{
private:int length;public:Box(int l) : length(l) {}// 声明友元函数friend ostream& operator<<(ostream& os, const Box& box);
};// 友元函数的定义
ostream& operator<<(ostream& os, const Box& box)
{os << "箱长:" << box.length;return os;
}int main()
{Box box(10);cout << box << endl;  // 使用重载的operator<<return 0;
}

输出:

箱长:10

在这个例子中,operator<<是一个友元函数,它被声明为Box类的友元,因此它可以访问Box类的私有成员length


3.2 友元类

友元类是指一个类的所有成员函数都可以成为另一个类的友元函数,因此友元类的所有成员函数都可以访问当前类的私有成员和保护成员。

友元类的特性:
  • 单向关系:如果类A声明类B为友元类,那么类B的成员函数可以访问类A的私有成员,但类A的成员函数不能访问类B的私有成员。友元关系是单向的(声明友元类B后,B能访问A,但A不能访问B)。
  • 不能继承:友元关系不能被继承,如果类A是类B的友元类,那么类B的派生类不会自动成为类A的友元类。
  • 关系不可传递:如果B是A的友元,C是B的友元,那么C并不是A的友元。
示例:友元类
class A
{int _x;
public:A(int x) : _x(x) {}friend class B; 			// 声明B为友元类
};class B
{
public:void AccessA(const A& a){cout << a._x << endl; 	// 访问A的私有成员}
};

再假设我们有Time类和Date类,并希望Time类可以访问Date类的私有成员。我们可以在Time类中声明Date为友元类。

#include <iostream>
using namespace std;class Time; // 前向声明class Date
{
public:void printTime(const Time& t); // 声明函数,访问 Time 类的私有成员
};class Time
{
private:int hour;int minute;
public:Time(int h, int m) : hour(h), minute(m) {}friend class Date; // 声明 Date 类为友元类
};void Date::printTime(const Time& t)
{cout << "Time: " << t.hour << ":" << t.minute << endl;
}int main()
{Time t(10, 30);Date d;d.printTime(t); // 输出:Time: 10:30return 0;
}

在这个例子中,Time类声明了Date类为友元类,这样Date类的成员函数printTime就能够访问Time类的私有成员(如hourminute)。

SO:

  • 友元函数:允许非成员函数访问类的私有和保护成员。它不是类的成员函数,通常用于操作符重载等场景。
  • 友元类:允许一个类的所有成员函数访问另一个类的私有和保护成员。它们的关系是单向的。
  • 使用场景:友元可以提高代码的灵活性和可操作性,但过多使用会增加类之间的耦合度,破坏封装,因此要谨慎使用。

4. 内部类(Nested Class)

4.1 概念:

内部类是定义在另一个类内部的类。它是一个独立的类,和外部类没有直接的归属关系,即它并不属于外部类的对象。外部类不能直接访问内部类的成员,反之,内部类可以访问外部类的成员,特别是当外部类的成员是static时(内部类天然是外部类的友元,可以访问外部类的私有成员)。

内部类有时也被称为嵌套类,通常用于将一个类作为一个辅助工具类嵌套在另一个类中,通常与外部类的功能紧密相关。

例如:

class A
{static int _count;
public:class B{public:void ShowCount(){cout << _count << endl; // 直接访问静态成员}};
};int A::_count = 10;int main()
{A::B b;b.ShowCount(); // 输出10
}
关键点:
  1. 独立性:内部类是一个完全独立的类,它不属于外部类的对象。因此,外部类不能通过自身的对象直接访问内部类的成员。
  2. 友元关系:虽然外部类不能直接访问内部类的成员,但内部类可以通过外部类的对象访问外部类的所有成员(包括私有成员)。从这个角度来看,内部类可以看作外部类的友元类(内部类天然具有可以访问外部类的私有成员这种访问权限,而不是因为它是友元类)。

4.2 内部类的特性

  1. 访问权限
    • 内部类可以定义在外部类的publicprotectedprivate等任何区域,和外部类的访问权限相同。
    • 但外部类不能直接访问内部类的成员,除非通过内部类的对象来访问。
    • 内部类可以访问外部类的非静态成员,但需要通过外部类的对象来访问。
  2. 访问外部类的静态成员
    • 内部类可以直接访问外部类中的static成员,无需外部类的对象或类名。这是因为静态成员是属于类本身的,而不是某个特定对象的。
  3. sizeof 外部类和内部类
    • 外部类和内部类是两个独立的实体,它们占用不同的内存空间。通过sizeof计算外部类的大小时,和内部类没有直接关系。

4.3 内部类的分类

  • 静态内部类static nested class):内部类是静态的,意味着它不需要外部类的实例就可以访问。静态内部类不能访问外部类的实例成员,但可以访问外部类的静态成员。
  • 非静态内部类(普通内部类):需要通过外部类的实例来创建实例,且能够访问外部类的所有成员(包括非静态成员)。

示例:内部类的使用

下面我们通过一个例子,详细讲解内部类的使用。

1. 普通的内部类
#include <iostream>
using namespace std;class Outer
{
private:int outerVar = 10;
public:class Inner{public:void accessOuter(Outer& outer){// 内部类可以访问外部类的私有成员cout << "访问外部类 private 成员:" << outer.outerVar << endl;}};
};int main()
{Outer outer;Outer::Inner inner;  			// 创建内部类对象inner.accessOuter(outer);  		// 通过内部类访问外部类的成员return 0;
}

输出:

访问外部类 private 成员:10

在这个例子中,InnerOuter类的内部类。Inner类可以访问Outer类的私有成员outerVar,并且我们通过Outer类的对象来访问外部类的成员。

2. 静态内部类
#include <iostream>
using namespace std;class Outer
{
private:static int staticVar;  		// 静态成员
public:static class Inner{public:void accessOuter(){// 静态内部类可以访问外部类的静态成员cout << "访问外部类 static 成员:" << staticVar << endl;}};
};int Outer::staticVar = 20;		// 类外初始化静态成员变量int main()
{Outer::Inner inner;  		// 创建静态内部类对象inner.accessOuter();  		// 静态内部类访问外部类的静态成员return 0;
}

输出:

访问外部类 static 成员:20

在这个例子中,InnerOuter类的静态内部类。静态内部类可以直接访问Outer类的静态成员staticVar,不需要外部类的对象。

3. 普通内部类和静态内部类的区别:
#include <iostream>
using namespace std;class Outer
{int outerVar = 10;
public:class Inner{public:void accessOuter(Outer& outer){cout << "非静态内部类:" << outer.outerVar << endl;}};static class StaticInner{public:void accessOuter(){cout << "static 内部类:" << outerVar << endl;  // 编译错误}};
};int main()
{Outer outer;Outer::Inner inner;inner.accessOuter(outer);  // 非静态内部类访问外部类成员Outer::StaticInner staticInner;// staticInner.accessOuter();  // 编译错误return 0;
}

SO:

  • 内部类是定义在另一个类内部的类。它是独立的类,外部类不能直接访问内部类的成员,内部类可以通过外部类的对象访问外部类的所有成员。
  • 友元关系:从某种角度看,内部类可以访问外部类的私有成员,所以可以视作外部类的友元类。
  • 内部类可以是静态的static),也可以是非静态的,它们的访问权限和行为有所不同。

内部类通常用于处理一些与外部类紧密相关的功能,帮助将代码组织得更好。


5. 匿名对象(Anonymous Object)

在C++中,匿名对象指的是没有明确名称的对象。它通常用于函数返回、临时数据传递、类型转换等场景,它们的生命周期仅限于它们所在的表达式或者函数调用,执行完毕后即被销毁。通过合理使用匿名对象,可以简化代码、减少不必要的对象创建,提高程序的效率。下面我将详细讲解匿名对象的相关知识点及其使用。

5.1 匿名对象的定义和创建

匿名对象是没有名字的临时对象。在C++中,匿名对象通常出现在以下场景:

  • 作为函数返回值: 当函数返回一个对象时,C++会创建一个匿名对象来接收返回值。
  • 临时对象: 用作表达式的操作数时,编译器会创建一个临时对象。
  • 类型转换: 在类型转换过程中,C++会临时创建匿名对象。

5.2 匿名对象的生命周期

匿名对象的生命周期非常短,通常只在一个表达式或者函数调用期间有效。它们会在表达式结束后立即销毁。这是因为它们没有名字,无法直接引用它们。

5.3 匿名对象的使用示例

示例 1:作为函数返回值的匿名对象
#include <iostream>
using namespace std;class MyClass
{
public:MyClass(){cout << "MyClass 构造函数" << endl;}~MyClass(){cout << "MyClass 析构函数" << endl;}void sayHello(){cout << "Hello from MyClass!" << endl;}
};// 函数返回匿名对象
MyClass createObject()
{return MyClass();  // 返回一个匿名对象
}int main()
{createObject().sayHello();  // 创建一个匿名对象并调用它的方法return 0;
}

解释:

  • createObject 函数中,return MyClass(); 创建了一个匿名对象并返回。
  • main 函数中,调用 createObject().sayHello() 时,匿名对象在 createObject 函数返回时创建,并且调用 sayHello() 方法。
  • 程序运行时,首先会打印构造函数的消息,接着打印 sayHello() 的消息,然后销毁匿名对象,打印析构函数的消息。
示例 2:作为临时对象
#include <iostream>
using namespace std;class MyClass
{
public:MyClass(){cout << "MyClass 构造函数" << endl;}~MyClass(){cout << "MyClass 析构函数" << endl;}void sayHello(){cout << "Hello from MyClass!" << endl;}
};int main()
{MyClass obj;obj.sayHello();MyClass().sayHello();  // 这里是一个匿名对象调用方法return 0;
}

解释:

  • main 函数中,MyClass().sayHello(); 创建了一个匿名对象并调用了 sayHello 方法。
  • 这个匿名对象仅在该行代码执行时有效,执行完后立即销毁。
示例 3:通过类型转换创建匿名对象
#include <iostream>
using namespace std;class MyClass
{
public:MyClass(int value) : m_value(value){cout << "MyClass 构造函数:" << m_value << endl;}~MyClass(){cout << "MyClass 析构函数" << endl;}private:int m_value;
};int main()
{MyClass(10);  // 创建一个匿名对象并传递给构造函数return 0;
}

解释:

  • MyClass(10); 创建了一个匿名对象,并且传递了参数 10 给构造函数。
  • 这个匿名对象在创建后立即销毁,生命周期仅限于该行代码。

5.4 匿名对象的应用场景

匿名对象有很多实际应用,下面列出一些常见的场景:

  1. 临时数据传递: 在函数调用时传递临时对象,避免了不必要的对象复制。

    例如:

    void processObject(const MyClass& obj)
    {// 处理传入的对象
    }processObject(MyClass(5));  // 创建一个匿名对象并传递
    
  2. 简化代码: 当不需要重复使用对象时,可以通过匿名对象来简化代码,避免创建多余的变量。

  3. 链式调用: 匿名对象可以用于链式调用多个函数。

    例如:

    MyClass().sayHello().anotherFunction();  // 链式调用匿名对象的方法
    

5.5 注意事项

  • 内存管理: 匿名对象通常是自动管理的,C++会在它们超出作用域后自动销毁。这意味着开发者不需要手动释放内存,但如果匿名对象涉及到动态内存分配(如 new),则需要特别注意内存管理。
  • 避免悬挂引用: 由于匿名对象的生命周期很短,必须避免在它销毁后访问它。

传道解惑

Q1:为什么匿名对象加const可以延长生命周期?

将匿名对象加上 const 修饰符,可以延长其生命周期。但这种延长的生命周期并不是无条件的,它的背后有一些特定的规则和原理。

在C++中,匿名对象的生命周期是由它们的作用域决定的,通常在一个表达式或函数调用结束时,匿名对象会被销毁。但是,如果将匿名对象声明为 const 类型,它将与一个引用绑定,从而延长其生命周期。这是因为 const 引用允许我们在对象生命周期结束后,依然通过引用来使用它。

具体解释:
  1. 匿名对象与临时对象的生命周期:
  • 默认情况下,匿名对象(临时对象)的生命周期通常非常短,仅限于它的表达式或语句的结束。例如:

    MyClass().doSomething();  // 匿名对象在 doSomething() 执行完后销毁
    
  1. 使用 const 引用延长生命周期:
  • 当匿名对象绑定到一个 const 引用时,C++会保证匿名对象的生命周期至少延长到该引用的生命周期结束。也就是说,这个引用会“延迟”对象销毁的时机,直到引用被销毁。
  • 关键点: const 引用可以延长临时对象的生命周期,使其存在于引用的作用域中,直到引用超出作用域。

示例:

void foo(const MyClass& obj)
{obj.doSomething();  // obj 是对匿名对象的引用
}int main()
{foo(MyClass());  // 匿名对象绑定到 const 引用 obj// 匿名对象在 foo() 返回时才销毁return 0;
}

在这个例子中:

  • MyClass() 创建了一个匿名对象。
  • 这个匿名对象会被传递给 foo() 函数,并绑定到 const MyClass& obj 上。
  • 匿名对象的生命周期被延长,直到 obj 超出作用域,也就是 foo() 函数结束。

临时对象的绑定规则:

  • 当临时对象(匿名对象)被绑定到一个const引用时,C++会延长临时对象的生命周期,直到引用超出作用域。
  • 这样做的目的是为了避免因临时对象提前销毁而导致引用悬挂问题(即引用一个已销毁的临时对象)。

例子:匿名对象与 const 引用

#include <iostream>
using namespace std;class MyClass
{
public:MyClass(){cout << "MyClass 构造函数" << endl;}~MyClass(){cout << "MyClass 析构函数" << endl;}void doSomething(){cout << "做点什么!" << endl;}
};void processObject(const MyClass& obj)
{obj.doSomething();  // 这里 obj 是对匿名对象的引用
}int main()
{processObject(MyClass());  // 匿名对象绑定到 const 引用 obj// 匿名对象的生命周期会被延长,直到 processObject 返回return 0;
}

输出:

MyClass 构造函数
做点什么!
MyClass 析构函数

解释:

  • processObject 函数中,MyClass() 创建了一个匿名对象,它被传递并绑定到 const MyClass& obj
  • 由于 objconst 引用,匿名对象的生命周期被延长,直到 processObject 函数返回。
  • 匿名对象的析构函数只会在 processObject 函数结束后调用。

重要说明:

  • const 引用延长生命周期的作用范围: const 引用的作用是延长临时对象的生命周期,直到引用超出作用域。这意味着匿名对象在引用的作用域内存在,引用超出作用域后,匿名对象才会销毁。而这样做是为了避免临时对象在使用时被提前销毁,确保引用对象的有效性。这种机制是C++中的一种特性,它通过引用的生命周期来保证匿名对象在函数作用域内的安全访问。
  • const 引用不允许绑定临时对象: 如果你尝试用一个非 const 引用绑定临时对象,C++ 编译器会报错,因为非 const 引用无法延长临时对象的生命周期。

Q2:匿名对象 VS 有名对象

匿名对象有名对象在C++中的最大区别在于命名、生命周期以及访问方式。匿名对象通常用于临时需要的场合,生命周期短,而有名对象则用于需要在多个地方访问和操作的情形,生命周期较长。下面我将详细解释:

命名

  • 匿名对象(Anonymous Object):顾名思义,它没有明确的名称,通常是临时创建的对象,不会绑定到变量上。

  • 例如:MyClass().doSomething();,在这里 MyClass() 创建了一个匿名对象,它没有名称,仅用于调用 doSomething() 方法。

  • 有名对象(Named Object):有明确的名称,可以通过变量名访问。

  • 例如:

    MyClass obj;  // obj 是有名对象
    obj.doSomething();
    

生命周期

  • 匿名对象:生命周期非常短暂,仅存在于当前表达式或者语句中,一旦使用完毕,匿名对象会被销毁。
  • 例如:MyClass().doSomething(); 中,匿名对象会在调用 doSomething() 后立刻销毁。
  • 有名对象:生命周期通常由它的作用域决定。对象在创建时分配内存,并在其作用域结束时被销毁。如果对象在栈上创建,它会在离开作用域时销毁;如果在堆上创建,则需要手动释放内存。
  • 例如:MyClass obj;obj 离开作用域时销毁。

访问方式

  • 匿名对象:不能通过变量名访问,因为它没有名称。只能在它创建的上下文中直接使用它。
  • 例如:MyClass().doSomething(); 中没有 MyClass 对象的名称,无法在之后访问它。
  • 有名对象:可以通过对象的名称来引用和访问对象的成员。
  • 例如:obj.doSomething(); 中,obj 是有名对象,可以在之后的代码中多次使用。

内存管理

  • 匿名对象:由于没有名称,它通常是栈上分配的,编译器在合适的时候自动管理内存。对于返回值优化(RVO/NRVO)等,编译器会优化创建匿名对象的内存管理,避免不必要的拷贝。
  • 有名对象:有名称,可以显式创建在栈上或堆上。栈上的对象在作用域结束时自动销毁,而堆上的对象则需要手动 delete

应用场景

匿名对象

  • 适用于临时使用,例如一次性计算或者在函数调用中使用临时对象。

  • 常见于函数返回值、类型转换、临时数据传递等场景。

  • 示例:

    void processObject(MyClass obj)
    {obj.doSomething();
    }processObject(MyClass());  // 匿名对象作为参数传递
    

有名对象

  • 适用于需要多次访问的场景,或者需要在多个地方使用该对象。

  • 典型用法是作为类的实例,创建时需要明确的对象名来进行后续操作。

  • 示例:

    MyClass obj;  // 有名对象
    obj.doSomething();  // 可以在后续访问该对象
    

返回值优化(RVO/NRVO)

  • 匿名对象在函数返回值时,编译器会尽可能优化,避免多余的拷贝操作,这被称为 返回值优化(RVO)或者 命名返回值优化(NRVO)。这意味着,返回匿名对象时,编译器会直接在调用位置构造返回对象,而不会创建临时对象。

  • 例如:

    MyClass createObject()
    {return MyClass();  // 匿名对象直接返回
    }
    
  • 有名对象没有这样的优化问题,通常会被拷贝或者移动到调用处,特别是在涉及对象返回时。

性能差异

  • 匿名对象:由于其生命周期非常短,编译器有时能够优化它们的创建和销毁过程,避免不必要的复制。
  • 在某些场景中,匿名对象能避免额外的内存分配和释放开销,提升性能。
  • 有名对象:虽然生命周期较长,但如果不合理使用,有时会增加额外的开销,尤其是在传递大对象时,可能会发生不必要的拷贝操作。

总结

特性匿名对象有名对象
命名没有名称,仅为临时对象有名称,可以通过变量名访问
生命周期短暂,仅在表达式或函数调用期间存在生命周期由作用域决定,作用域结束时销毁
访问方式不能直接访问,通常仅在当前表达式中使用可以通过名称多次访问
内存管理编译器自动管理内存,通常是栈上分配可以是栈上或堆上,需要显式管理堆对象的内存
应用场景临时数据传递、返回值、一次性计算等需要多次使用、存储数据或状态等
性能差异编译器优化可能避免不必要的复制如果不小心使用,可能有不必要的拷贝操作### 总结

6. 再次理解类和对象

理解对象的概念,能帮助我们更好地理解面向对象编程(OOP)的核心思想。为了更通俗地讲解这个内容,我们可以通过一个现实中的例子来帮助理解。

1. 类是对事物的抽象

类就像是对某种事物(例如洗衣机)的抽象描述。它是对现实中事物的建模,在程序中描述这个事物的属性行为

  • 属性:就是这个事物的特征,比如洗衣机的品牌、颜色、容量等。
  • 行为:就是这个事物可以做的事情,比如洗衣机可以“启动”、“停止”、“洗衣服”等。

举个例子,洗衣机这个事物可以用类来描述:

class WashingMachine
{
public:string brand;      // 洗衣机的品牌string color;      // 洗衣机的颜色int capacity;      // 洗衣机的容量(比如5kg、10kg)void start(){cout << "Washing machine started." << endl;   	 // 启动洗衣机}void stop(){cout << "Washing machine stopped." << endl;      // 停止洗衣机}
};

在这个例子中,WashingMachine类描述了一个洗衣机的属性brand, color, capacity)和行为start()stop())。这个类的作用就是抽象化洗衣机,将它的特征和行为描述给计算机。

2. 对象是类的实例化

类是对现实事物的抽象描述,但计算机无法直接“认识”类,必须通过实例化类来创建对象,而对象才是计算机可以操作的具体实体。

  • 实例化:就是通过类创建具体的对象的过程。
  • 对象:是类的具体实例,表示现实世界中的某个具体的事物。比如,你可以通过WashingMachine类创建多个洗衣机对象,每个对象代表一个具体的洗衣机。

例子继续,假设我们现在创建了一个洗衣机对象:

#include <iostream>
using namespace std;class WashingMachine
{
public:string brand;      // 洗衣机的品牌string color;      // 洗衣机的颜色int capacity;      // 洗衣机的容量(单位:kg)void start(){cout << "Washing machine started." << endl;}void stop(){cout << "Washing machine stopped." << endl;}void wash(){cout << "Washing clothes..." << endl;}
};int main()
{WashingMachine wm1;wm1.brand = "小米";wm1.color = "白色";wm1.capacity = 10;wm1.start();  // 启动洗衣机wm1.wash();   // 洗衣服wm1.stop();   // 停止洗衣机WashingMachine wm2;wm2.brand = "格力";wm2.color = "黑色";wm2.capacity = 8;wm2.start();wm2.wash();wm2.stop();return 0;
}

在这个例子中,wm1wm2WashingMachine类的两个对象。它们分别代表两个不同的洗衣机,每个对象的属性(brand, color, capacity)可以有不同的值。通过这些对象,我们可以模拟现实中的多个洗衣机。

3. 通过类创建对象

从以上的例子可以看出,类只是描述了洗衣机的属性和行为,而对象才是具体的实例。你可以通过类创建出多个对象,每个对象都代表一个具体的事物。类就像是一个模板或蓝图,具体的对象是根据这个模板生成的。

4. 类和对象的关系

  • 是对事物的一种描述,它定义了这个事物的属性行为
  • 对象是类的实例,是计算机可以直接操作的具体实体。通过类可以创建多个对象,每个对象都有不同的属性值和方法。

总结一下,类和对象的关系可以类比为:

  • :就像是一本描述洗衣机的说明书,它告诉我们洗衣机有哪些属性(品牌、颜色、容量)和行为(启动、停止)。
  • 对象:就像是根据这本说明书实际生产出来的具体洗衣机。每一台洗衣机都有自己的品牌、颜色、容量等信息,并可以执行启动、停止等操作。

5. 现实中的例子:洗衣机类

让我们通过现实中的洗衣机来进一步理解。

  1. 抽象洗衣机:当我们想到洗衣机时,我们并不会想到具体某一台洗衣机,而是先想到了“洗衣机”这个概念。它有品牌、颜色、容量这些特征,并且有启动、停止这些操作。这就是的作用:把这些共性的特征和行为总结出来。
  2. 创建洗衣机对象:当你去买洗衣机时,你选择了一个品牌、颜色、容量等具体参数的洗衣机。每一台洗衣机就是一个对象,它是类的实例化。
  3. 操作洗衣机:当你开始使用这台洗衣机时,你可以通过按按钮来“启动”和“停止”,这就是对象通过类提供的操作(方法)来实现的行为。

SO:

  • 是对现实世界中事物的抽象描述,它总结了事物的属性行为
  • 对象是类的具体实例,是计算机能够识别和操作的实体。
  • 是对事物的抽象描述,而对象是根据类创建的具体实例。
  • 通过,我们可以创建多个不同的对象,每个对象具有类中定义的属性和行为。

通过理解类和对象的关系,你将能够更好地理解面向对象编程(OOP)的核心思想,这对于学习和使用C++、Java等面向对象语言非常重要。

共勉

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/10392.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot笔记

1.创建 使用idea提供的脚手架创建springboot项目&#xff0c;选上需要的模块&#xff0c;会自动进行导包 打成jar包&#xff0c;之前直接用原生的maven打包的是一个瘦jar&#xff0c;不能直接跑&#xff0c;把服务器上部署的jar排除在外了&#xff0c;但是现在加上打包查件&am…

Fiddler(一) - Fiddler简介_fiddler软件

文章目录 一、为什么选择Fiddler作为抓包工具? 二、什么是Fiddler?三、Fiddler使用界面简介四、延伸阅读 一、为什么选择Fiddler作为抓包工具? 抓包工具有很多&#xff0c;小到最常用的web调试工具firebug&#xff0c;大到通用性强大的抓包工具wireshark。为什么使用fid…

受击反馈HitReact、死亡效果Death Dissolve、Floating伤害值Text(末尾附 客户端RPC )

受击反馈HitReact 设置角色受击标签 (GameplayTag基本了解待补充) 角色监听标签并设置移动速度 创建一个受击技能&#xff0c;并应用GE 实现设置角色的受击蒙太奇动画 实现角色受击时播放蒙太奇动画&#xff0c;为了保证通用性&#xff0c;将其设置为一个函数&#xff0c;并…

Vue+Echarts 实现青岛自定义样式地图

一、效果 二、代码 <template><div class"chart-box"><chart ref"chartQingdao" style"width: 100%; height: 100%;" :options"options" autoresize></chart></div> </template> <script> …

线段树(Segment Tree)和树状数组

线段树&#xff08;Segment Tree&#xff09;和树状数组 线段树的实现链式&#xff1a;数组实现 解题思路树状数组 线段树是 二叉树结构 的衍生&#xff0c;用于高效解决区间查询和动态修改的问题&#xff0c;其中区间查询的时间复杂度为 O(logN)&#xff0c;动态修改单个元素的…

计算机网络 (62)移动通信的展望

一、技术发展趋势 6G技术的崛起 内生智能&#xff1a;6G将强调自适应网络架构&#xff0c;通过AI驱动的智能算法提升通信能力。例如&#xff0c;基于生成式AI的6G内生智能架构将成为重要研究方向&#xff0c;实现低延迟、高效率的智能通信。信息编码与调制技术&#xff1a;新型…

数据结构 前缀中缀后缀

目录 前言 一&#xff0c;前缀中缀后缀的基本概念 二&#xff0c;前缀与后缀表达式 三&#xff0c;使用栈实现后缀 四&#xff0c;由中缀到后缀 总结 前言 这里学习前缀中缀后缀为我们学习树和图做准备&#xff0c;这个主题主要是对于算术和逻辑表达式求值&#xff0c;这…

音视频多媒体编解码器基础-codec

如果要从事编解码多媒体的工作&#xff0c;需要准备哪些更为基础的内容&#xff0c;这里帮你总结完。 因为数据类型不同所以编解码算法不同&#xff0c;分为图像、视频和音频三大类&#xff1b;因为流程不同&#xff0c;可以分为编码和解码两部分&#xff1b;因为编码器实现不…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型&#xff0c;用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说&#xff0c;N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意&#xff1a;这…

Windows系统本地部署deepseek 更改目录

本地部署deepseek 无论是mac还是windows系统本地部署deepseek或者其他模型的命令和步骤是一样的。 可以看: 本地部署deepsek 无论是ollama还是部署LLM时候都默认是系统磁盘&#xff0c;对于Windows系统&#xff0c;我们一般不把应用放到系统盘&#xff08;C:&#xff09;而是…

知识库管理如何推动企业数字化转型与创新发展的深层次探索

内容概要 在当今数字化转型的大背景下&#xff0c;知识库管理日益显现出其作为企业创新发展的核心驱动力的潜力。这种管理方式不仅仅是对信息的存储与检索&#xff0c;更是一种赋能&#xff0c;以提升决策效率和员工创造力。企业能够通过系统地整合和管理各类知识资源&#xf…

商品列表及商品详情展示

前言 本文将展示一段结合 HTML、CSS 和 JavaScript 的代码&#xff0c;实现了一个简单的商品展示页面及商品详情&#xff0c;涵盖数据获取、渲染、搜索及排序等功能。 效果展示 点击不同的商品会展示对应的商品详情。 代码部分 代码总体实现 <!DOCTYPE html> <htm…

论文阅读:Realistic Noise Synthesis with Diffusion Models

这篇文章是 2025 AAAI 的一篇工作&#xff0c;主要介绍的是用扩散模型实现对真实噪声的仿真模拟 Abstract 深度去噪模型需要大量来自现实世界的训练数据&#xff0c;而获取这些数据颇具挑战性。当前的噪声合成技术难以准确模拟复杂的噪声分布。我们提出一种新颖的逼真噪声合成…

【汽车电子架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子架构&#xff1a;AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风&#xff0c;从概念与定义入手&#xff0c;希望读者能对AutoSAR架构有一个整体的认识&#xff0c;然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

毕业设计--具有车流量检测功能的智能交通灯设计

摘要&#xff1a; 随着21世纪机动车保有量的持续增加&#xff0c;城市交通拥堵已成为一个日益严重的问题。传统的固定绿灯时长方案导致了大量的时间浪费和交通拥堵。为解决这一问题&#xff0c;本文设计了一款智能交通灯系统&#xff0c;利用车流量检测功能和先进的算法实现了…

FPGA|使用quartus II通过AS下载POF固件

1、将开发板设置到AS下载挡位&#xff0c;或者把下载线插入到AS端口 2、打开quartus II&#xff0c;选择Tools→Programmer→ Mode选择Active Serial Programming 3、点击左侧Add file…&#xff0c;选择 .pof 文件 →start 4、勾选program和verify&#xff08;可选&#xff0…

适合超多氛围灯节点应用的新选择

文章目录 1.前言2.芯片简介2.1 高动态RGB照明2.2 灵活的通信模式2.3 精确的颜色控制2.4 高性能与可靠性2.5 易于集成与控制 3.硬件介绍3.1 方案框图3.2 通信模式3.3 器件选型 4.基础操作4.1 基础操作示例4.2 状态机4.3 启动行为4.4 诊断 5 颜色控制和温度稳定化5.1 颜色控制5.2…

MATLAB-Simulink并行仿真示例

一、概述 在进行simulink仿真的过程中常常遇到CPU利用率较低&#xff0c;仿真缓慢的情况&#xff0c;可以借助并行仿真改善这些问题&#xff0c;其核心思想是将参数扫描、蒙特卡洛分析或多工况验证等任务拆分成多个子任务&#xff0c;利用多核CPU或计算集群的并行计算能力&…

运算符重载(输出运算符<<) c++

我们来看下面这个Bug 报错1&#xff1a;打印整形&#xff08;int&#xff09;可以直接打印&#xff0c;打印字符&#xff08;char&#xff09;也可以直接打印&#xff0c;那是因为本身就已经给我们的内置类型准备好了一个输出运算符&#xff0c;可以直接用&#xff0c;但是我们…

【C++】类和对象(5)

目录 一、构造函数补充1、初始化列表 二、类型转换三、static成员四、友元1、友元函数2、友元类 五、内部类六、匿名对象 一、构造函数补充 对于之前讲解的构造函数&#xff0c;还有一些更深层次的内容要进行补充&#xff0c;接下来进行补充内容的讲解。 1、初始化列表 在我…