探索C++中的类与对象:构建程序的基石(下)

在这里插入图片描述


前言

继上篇探索C++中的类与对象:构建程序的基石(上)
探索C++中的类与对象:构建程序的基石(中)
在编程的世界里,C++以其强大的灵活性和高效性,在众多编程语言中占据了举足轻重的地位。它不仅继承了C语言的底层操作能力和高效执行速度,还引入了面向对象编程(OOP)的概念,极大地提升了代码的可维护性、可扩展性和重用性。其中,类和对象是C++面向对象编程的核心,它们为程序员提供了一种组织代码、模拟现实世界实体以及实现复杂数据结构的有效方式。


💫1. 再谈构造函数

1.1 函数体内赋值

在构造函数体内进行赋值,即对象的成员变量先通过默认构造函数创建,随后在构造函数体内被赋值。

class Date{
public:
Date(int year, int month, int day){
  _year = year;
  _month = month;
  _day = day;
}
private:
int _year;
int _month;
int _day;
};

缺点:

  • 效率较低:在构造函数体内赋值时,成员变量已经经历了一次默认初始化,之后再进行赋值。这会导致两步操作,特别是对于复杂类型对象,可能导致不必要的性能损耗。
  • 无法处理某些成员类型:对于 const 成员、引用类型、以及没有默认构造函数的类成员,无法使用这种方式赋值,必须使用初始化列表。

1.2 初始化列表

初始化列表是在构造函数的声明后,紧跟着冒号 : 的一部分。它在对象创建时,直接调用成员变量的构造函数或对其进行初始化。

class Date{
public:
Date(int year, int month, int day)
  : _year(year)
  , _month(month)
  , _day(day)
{}
private:
int _year;
int _month;
int _day;
};

【注意】

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
class A{
public:
    A(int a)
        :_a(a)
    {}
private:
    int _a;
};

class B{
public:
    // 初始化列表:对象的成员定义的位置
    B(int a, int& ref)
        : _aobj(a)
        , _ref(ref)
        , _n(10)    
    {}
private:
    A _aobj;		// 没有默认构造函数

    // 特征:必须在定义的时候初始化
    int& _ref;		// 引用
    const int _n;	// const
};

【注意】

在B类的初始化列表中,为什么使用int& ref而不是int ref,原因是ref如果是局部变量,那么出了作用域就销毁了,此时_ref就相当于野引用,所以这里应该是int&

优点:

  • 效率高:初始化列表直接在对象创建时初始化成员变量,避免了先默认构造再赋值的额外步骤。
  • 强制初始化:某些类型(如const引用)必须通过初始化列表进行初始化。
  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序
class A{
public:
    A(int a): _a1(a), _a2(_a1){}
    void Print(){
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};

int main()    
{	
    A aa(1);
    aa.Print();

	return 0;
}

// A. 输出1  1
// B. 程序崩溃
// C. 编译不通过
// D. 输出1  随机值

💫2. Static静态成员

2.1 静态成员变量

静态成员变量(也称为类变量)是指在面向对象编程中,属于类而不是某个特定对象的变量。它的特性是在类的所有实例之间共享,即无论创建了多少个对象,静态成员变量在内存中只有一个副本,所有实例对这个变量的修改都会反映在所有其他实例中。

  1. 属于类本身:静态成员变量是类级别的,不能通过对象直接定义,而是通过类定义。
  2. 共享性:所有对象共享同一个静态成员变量,修改这个变量时,所有的实例都会感知到修改的值。
  3. 生命周期长:静态成员变量随着类的加载而存在,类卸载时才会消失。
  4. 访问方式:可以通过类名直接访问,而不需要实例化对象
#include<iostream>
using namespace std;

class MyClass {
public:
  static int staticVar;

  void display() {
      cout << "Static Variable: " << staticVar << endl;
  }
};

int MyClass::staticVar = 10;  // 初始化静态成员变量

int main() {
  MyClass obj1, obj2;

  obj1.display();  // 输出:Static Variable: 10
  obj2.display();  // 输出:Static Variable: 10

  obj1.staticVar = 20;  // 修改静态变量

  obj1.display();  // 输出:Static Variable: 20
  obj2.display();  // 输出:Static Variable: 20

  return 0;
}

在这个例子中,staticVarMyClass的静态成员变量。即使obj1obj2是不同的实例,但它们都共享同一个staticVar变量。当obj1修改了staticVar的值,obj2也会看到同样的变化。

注意

  • 静态成员变量的初始化必须在类定义外进行。
  • 不能通过对象直接初始化静态成员变量。

2.2 静态成员函数

静态成员函数是与类相关联的函数,而不是与类的具体实例关联。它属于类本身,而不是类的某个对象。静态成员函数在使用时无需实例化对象,可以直接通过类名调用。不依赖对象:静态成员函数是类级别的函数,不依赖于类的具体对象。它可以在没有实例化类对象的情况下直接调用。

  1. 不能访问非静态成员变量和非静态成员函数:由于静态成员函数不依赖于对象,它不能直接访问类的非静态成员变量或非静态成员函数,因为这些成员变量和成员函数是依赖于具体对象的。

  2. 可以访问静态成员变量:静态成员函数可以访问静态成员变量,因为静态成员变量同样是类级别的,与对象无关。

  3. 常用于工具函数或与实例无关的逻辑:静态成员函数常用于执行与具体对象无关的任务,比如全局计数、工具函数等。

#include<iostream>
using namespace std;

class MyClass {
public:
  static int staticVar;  // 静态成员变量

  // 静态成员函数
  static void staticFunction() {
      cout << "Static Variable: " << staticVar << endl;
  }

  // 非静态成员函数
  void nonStaticFunction() {
      cout << "Non-static function can access staticVar: " << staticVar << endl;
  }
};

int MyClass::staticVar = 10;  // 初始化静态成员变量

int main() {
  // 调用静态成员函数,不需要创建对象
  MyClass::staticFunction();  // 输出:Static Variable: 10

  // 修改静态成员变量的值
  MyClass::staticVar = 20;
  MyClass::staticFunction();  // 输出:Static Variable: 20

  // 创建对象,调用非静态成员函数
  MyClass obj;
  obj.nonStaticFunction();  // 输出:Non-static function can access staticVar: 20

  return 0;
}

解释

  • 静态成员变量 staticVar:这是一个静态成员变量,属于整个类。无论创建多少个对象,它的值在所有对象间是共享的。
  • 静态成员函数 staticFunction:可以通过类名 MyClass::staticFunction() 调用,无需创建对象。它能访问静态成员变量 staticVar
  • 非静态成员函数 nonStaticFunction:它可以访问静态成员变量staticVar,因为静态成员变量对整个类可见。

使用静态成员函数的场景

  1. 与对象无关的操作:当函数的逻辑不依赖具体的对象时,可以使用静态成员函数,例如工具类中的数学计算方法。
  2. 访问或操作静态成员变量:静态成员函数常用于操作静态成员变量,例如维护类实例的全局计数等。
  3. 工厂模式:静态成员函数可以用于返回类的实例,如工厂模式中常用的“创建对象”的函数。

注意事项

  • 静态成员函数无法直接调用非静态成员变量和非静态成员函数。如果需要访问,必须传递对象实例或者将非静态成员变量变为静态成员变量。
  • 静态成员函数虽然不依赖于对象,但是它们同样遵守类的访问控制(如privateprotected)。

💫3. 友元

友元的本质: 友元打破了 C++ 封装的严格限制,使得指定的外部函数或类能够访问类的私有成员和保护成员。友元并不是类的成员,它是一种特殊的外部“访问权限声明”。

3.1 友元的类型:

  • 友元函数:普通的函数可以通过在类内声明为友元,从而可以访问该类的私有和保护成员。
  • 友元类:一个类可以将另一个类声明为友元,这样友元类的所有成员函数都能访问该类的私有和保护成员。
  • 友元成员函数:某类的特定成员函数可以被声明为友元,只对该特定函数开放访问权限。

3.2 友元的应用场景:

  • 操作符重载:特别是像 <<>> 这样的输入输出运算符重载,通常需要通过友元函数来访问类的私有数据。
  • 调试和日志:通过友元,某些调试类可以直接访问目标类的内部状态,用于日志记录或状态检查。

示例 - 操作符重载中的友元函数

class Complex {
private:
   double real, imag;
public:
   Complex(double r, double i) : real(r), imag(i) {}

   // 声明友元函数
   friend std::ostream& operator<<(std::ostream& out, const Complex& c);
};

// 友元函数定义
std::ostream& operator<<(std::ostream& out, const Complex& c) {
   out << c.real << " + " << c.imag << "i";
   return out;
}

在这个例子中,<< 运算符被声明为 Complex 类的友元函数,从而能够访问 Complex 的私有成员 realimag

💫4. 内部类

内部类的本质: 内部类是一个类的成员,存在于另一个类的定义内部。它与外部类存在某种逻辑关系,但不会自动继承访问权限。通常,内部类用于表示外部类的一个组成部分,封装复杂的数据结构或功能。

4.1 内部类的访问规则:

  • 内部类和外部类之间的访问权限是独立的,除非明确声明为友元。
  • 外部类不能直接访问内部类的私有成员,反之亦然。
  • 内部类可以访问外部类的公有和保护成员。

4.2 内部类的应用场景:

  • 实现细节封装:内部类经常用来封装外部类的实现细节,隐藏复杂的内部逻辑。
  • 模块化设计:内部类可以用于实现更清晰的模块化设计,将一个大类拆分成多个小的内部类来管理不同的功能。

示例 - 内部类与外部类的交互

class Outer {
private:
   int outerValue;
public:
   Outer(int val) : outerValue(val) {}

   // 内部类声明
   class Inner {
   public:
       void displayOuter(Outer& o) {
           std::cout << "Outer class value: " << o.outerValue << std::endl; // 访问外部类的私有成员
       }
   };
};

在这个例子中,Inner 类是 Outer 类的内部类,Inner 类的 displayOuter 函数可以访问 Outer 类的私有成员。

💫5. 匿名对象

匿名对象的本质: 匿名对象是未被命名的对象,它通常是在表达式中临时生成的,生命周期极短。匿名对象常见于临时对象的创建和函数返回值中。匿名对象的好处是避免了不必要的命名和生命周期管理,简化了代码逻辑。

5.1 匿名对象的特点:

  • 自动销毁:匿名对象在使用完后立即销毁,不占用额外的资源。
  • 适用于短期操作:非常适合在函数调用中返回临时对象,避免了拷贝和对象管理的复杂性。

5.2 匿名对象的应用场景:

  • 临时计算结果:某些场景下,使用匿名对象来计算临时结果非常常见。
  • 返回值优化:在函数返回值时,匿名对象与返回值优化(RVO)结合,能有效减少拷贝。

示例 - 匿名对象作为返回值

class Example {
public:
   Example() {
       std::cout << "Constructor called!" << std::endl;
   }
   ~Example() {
       std::cout << "Destructor called!" << std::endl;
   }
};

Example createExample() {
   return Example();  // 返回匿名对象
}

int main() {
   Example e = createExample();  // 用匿名对象初始化 e
}

在这里,Example() 是匿名对象,它的生命周期仅限于函数调用,它的构造和析构顺序也表明了其生命周期的短暂性。

💫6. 拷贝对象时的一些编译器优化

编译器在处理对象拷贝时,会进行一些常见的优化以提高性能。以下是几种主要的优化技术:

6.1 返回值优化(RVO

RVO 是一种编译器优化,它避免了在函数返回时临时对象的拷贝构造。编译器在函数返回时直接在目标位置创建对象,消除了拷贝的开销。

示例

class A {
public:
   A() { std::cout << "Constructor" << std::endl; }
   A(const A&) { std::cout << "Copy Constructor" << std::endl; }
};

A createA() {
   return A();  // RVO,避免拷贝
}

int main() {
   A a = createA();  // RVO 使得没有调用拷贝构造函数
}

6.2 移动语义

C++11 引入了移动语义,通过移动构造函数和移动赋值运算符,能够有效避免深拷贝的开销。移动语义将对象资源的所有权转移,而不是进行拷贝。

示例

class B {
public:
   B() { std::cout << "Constructor" << std::endl; }
   B(B&&) { std::cout << "Move Constructor" << std::endl; }
};

B createB() {
   return B();  // 移动构造
}

此例中,移动语义会避免不必要的深拷贝,大大提升性能。

6.3 拷贝省略

在某些情况下,C++ 标准允许编译器跳过某些不必要的拷贝操作,比如在函数返回时,编译器直接在调用者的上下文中构造返回对象,避免了临时对象的创建和拷贝。

💫7. 再次理解封装

封装的本质: 封装是面向对象编程(OOP)的核心原则之一。它通过将对象的状态(数据)和行为(方法)封装在类中,限制外部对类内部实现的直接访问。封装的目的是保护对象的完整性,并通过控制访问权限实现信息隐藏。

7.1 封装的三种访问控制:

  1. Public(公有):外部可以自由访问,表示开放给外部的接口。
  2. Private(私有):外部无法访问,只有类的内部成员函数可以访问。
  3. Protected(保护):子类可以访问,但外部类无法访问。

7.2 封装的优势:

  • 数据安全性:通过私有和保护成员变量,封装可以保护数据的完整性,避免外部直接修改数据,确保程序的稳定性和安全性。
  • 灵活性:通过封装,内部实现可以随时更改,而不影响外部代码,因为外部只能通过公开接口与对象交互。
  • 降低耦合:封装可以减少类之间的依赖和耦合,提高代码的可维护性和可扩展性。

示例 - 封装的好处

class BankAccount {
private:
   double balance;  // 私有数据成员,外部无法直接访问
public:
   BankAccount(double initBalance) : balance(initBalance) {}

   void deposit(double amount) {
       if (amount > 0) {
           balance += amount;
       }
   }

   void withdraw(double amount) {
       if (amount > 0 && amount <= balance) {
           balance -= amount;
       }
   }

   double getBalance() const {
       return balance;
   }
};

通过封装,balance 变量不会被外部代码直接修改,外部只能通过 depositwithdraw 函数来


结语

通过本文的学习,相信你已经对C++中的类和对象有了全面而深入的理解。类和对象不仅是C++面向对象编程的基础,更是现代软件开发不可或缺的工具。它们教会我们如何以更加抽象和模块化的方式思考问题,将复杂的系统分解成简单、可管理的部分。掌握这一技能,你将能够设计出更加灵活、健壮的软件系统,应对日益复杂的编程挑战。
在这里插入图片描述

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值