04--C++ 类和对象下篇

【本节目标】

  1. 再谈构造函数
  2. Static 成员
  3. 友元
  4. 内部类
  5. 匿名对象
  6. 拷贝对象时的一些编译器优化
  7. 再次理解封装

1. 再谈构造函数

1.1 构造函数体赋值

创建对象时,编译器通过构造函数给成员变量赋初始值,但构造函数体中的操作只能称为 "赋初值",而非 "初始化"。

  • 核心区别:初始化只能执行一次,而构造函数体中可对成员变量多次赋值。
cpp 复制代码
class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;  // 赋初值(可多次执行)
        _month = month;
        _day = day;
        _year = 2024;  // 再次赋值,合法
    }
private:
    int _year;
    int _month;
    int _day;
};

1.2 初始化列表

初始化列表是成员变量真正的初始化场所,语法格式:以冒号开头,逗号分隔成员变量,每个成员后接括号包裹的初始值 / 表达式。

cpp 复制代码
class Date
{
public:
    // 初始化列表初始化
    Date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

!note\] 实际上没有什么好处,只是两种写法不同用而已 !\[\[C++类和对象下篇.png\]

核心注意事项
  1. 每个成员变量在初始化列表中只能出现一次 (初始化仅能执行一次);
    2. 以下成员必须在初始化列表中初始化:
    • 引用成员变量(引用必须初始化,且不能更改指向);
    • const 成员变量(const 变量必须初始化,且不能修改);
    • 自定义类型成员(该类无默认构造函数时,必须显式传参初始化);
cpp 复制代码
   class A
   {
   public:
       A(int a) : _a(a) {}  // 无默认构造函数
   private:
       int _a;
   };
   
   class B
   {
   public:
       // 必须在初始化列表初始化以下成员
       B(int a, int ref)
           : _aobj(a)  // 自定义类型A无默认构造
           , _ref(ref)  // 引用成员
           , _n(10)    // const成员
       {}
   private:
       A _aobj;
       int& _ref;
       const int _n;
   };
   ```
2. 自定义类型成员变量,无论是否显式写在初始化列表,都会**优先通过初始化列表初始化**(调用其默认构造函数);

 ```cpp
   class Time
   {
   public:
       Time(int hour = 0) : _hour(hour)
       {
           cout << "Time()" << endl;  // 会被调用
       }
   private:
       int _hour;
   };
   
   class Date
   {
   public:
       Date(int day) {}  // 未显式初始化_time,但会默认初始化
   private:
       int _day;
       Time _t;  // 自定义类型成员
   };
   
   int main()
   {
       Date d(1);  // 输出:Time()
       return 0;
   }
   ```

3. 成员变量的初始化顺序,**取决于其在类中的声明顺序**,与初始化列表中的先后次序无关;


 ```cpp
   class A
   {
   public:
       A(int a)
           : _a1(a)
           , _a2(_a1)  // 初始化顺序:_a2先声明,先初始化(此时_a1未初始化,值为随机)
       {}
       void Print() { cout << _a1 << " " << _a2 << endl; }
   private:
       int _a2;  // 声明次序1
       int _a1;  // 声明次序2
   };
   
   int main()
   {
       A aa(1);
       aa.Print();  // 输出:1 随机值(答案选D)
       return 0;
   }
   ```

临时笔记:
临时变量具有常性;一般要用const修饰;


### 1.3 explicit 关键字

单参构造函数(或除第一个参数外其余均有默认值的构造函数),默认支持**隐式类型转换**(用其他类型值构造临时对象,再赋值给当前对象)。<font color="#ff0000">`explicit`关键字可禁止该隐式转换,提升代码可读性。</font>

```cpp
class Date
{
public:
   // 单参构造函数(无explicit,支持隐式转换)
   Date(int year) : _year(year) {}

   Date& operator=(const Date& d)
   {
       if (this != &d)
       {
           _year = d._year;
           _month = d._month;
           _day = d._day;
       }
       return *this;
   }
private:
   int _year = 1;
   int _month = 1;
   int _day = 1;
};

void Test()
{
   Date d1(2022);
   d1 = 2023;  // 隐式转换:2023 → 临时Date对象 → 赋值给d1
}
禁止隐式转换(加 explicit)
cpp 复制代码
class Date
{
public:
    // explicit修饰,禁止隐式类型转换
    explicit Date(int year) : _year(year) {}

    Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }
private:
    int _year = 1;
    int _month = 1;
    int _day = 1;
};

void Test()
{
    Date d1(2022);
    // d1 = 2023;  // 编译报错:禁止隐式转换
}

扩展场景多参数但部分有默认值的构造函数,也支持隐式转换,explicit同样可禁止:

cpp 复制代码
explicit Date(int year, int month = 1, int day = 1)
    : _year(year), _month(month), _day(day)
{}

2. Static 成员

2.1 概念

声明为static的类成员,称为静态成员:

  • 静态成员变量:用static修饰的成员变量(类内声明,类外初始化);
  • 静态成员函数:用static修饰的成员函数(无隐藏this指针)。
经典面试题:统计类对象创建个数
cpp 复制代码
class A
{
public:
    A() { ++_scount; }          // 构造函数:创建对象+1
    A(const A& t) { ++_scount; } // 拷贝构造:创建对象+1
    ~A() { --_scount; }          // 析构函数:销毁对象-1
    static int GetACount() { return _scount; } // 静态成员函数:获取对象个数
private:
    static int _scount; // 静态成员变量:统计对象个数(类内声明)
};

int A::_scount = 0; // 类外初始化:不加static关键字

void TestA()
{
    cout << A::GetACount() << endl; // 输出:0(无对象)
    A a1, a2;
    A a3(a1); // 拷贝构造创建对象
    cout << A::GetACount() << endl; // 输出:3(3个对象)
}

2.2 核心特性

  1. 静态成员为所有类对象共享,不属于某个具体对象,存储在静态区;

  2. 静态成员变量必须在类外定义 ,定义时不添加static关键字(类内仅声明);

  3. 访问方式:类名::静态成员对象.静态成员(两种均可);

    cpp 复制代码
    int main()
    {
        A a;
        cout << A::GetACount() << endl; // 类名访问
        cout << a.GetACount() << endl;  // 对象访问
        return 0;
    }
  4. 静态成员函数无隐藏this指针,不能访问任何非静态成员(非静态成员依赖具体对象);

  5. 静态成员受访问限定符(public/protected/private)限制(如私有静态成员,类外无法访问)。

思考问题(面试高频)
  1. 静态成员函数可以调用非静态成员函数吗?

    → 不能(无this指针,无法访问非静态成员);

  2. 非静态成员函数可以调用静态成员函数吗?

    → 可以(静态成员属于类,无需具体对象,可直接调用)。

3. 友元

友元是突破类封装的特殊机制,允许外部函数 / 类直接访问类的私有 / 保护成员,但会增加耦合度、破坏封装,不宜多用。

友元分为:友元函数、友元类。

3.1 友元函数

问题背景

重载operator<<(输出流运算符)时,若作为成员函数,this指针会抢占第一个参数位置(左操作数),导致调用形式不符合常规(如d1 << cout)。因此需重载为全局函数,但全局函数无法访问类的私有成员,需通过友元解决。

友元函数定义

类内声明全局函数,加friend关键字,该函数即可直接访问类的私有 / 保护成员(不属于类,仍是普通全局函数)。

cpp 复制代码
class Date
{
    // 声明友元函数:operator<<和operator>>
    friend ostream& operator<<(ostream& _cout, const Date& d);
    friend istream& operator>>(istream& _cin, Date& d);
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

// 友元函数实现(类外,无需加friend)
ostream& operator<<(ostream& _cout, const Date& d)
{
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
}

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

// 调用示例
int main()
{
    Date d;
    cin >> d;         // 正常调用:istream& operator>>(cin, d)
    cout << d << endl; // 正常调用:ostream& operator<<(cout, d)
    return 0;
}
友元函数特性
  1. 可访问类的私有 / 保护成员,但不是类的成员函数
  2. 不能用const修饰(无this指针,const修饰无意义);
  3. 类内声明位置不受访问限定符限制(public/private均可);
  4. 一个函数可作为多个类的友元函数;
  5. 调用方式与普通全局函数一致。

3.2 友元类

友元类的所有成员函数,均为另一个类的友元函数,可直接访问其非公有成员。

核心特性
  1. 友元关系单向:A 是 B 的友元,不代表 B 是 A 的友元;
  2. 友元关系不可传递:A 是 B 的友元,B 是 C 的友元,不代表 A 是 C 的友元;
  3. 友元关系不可继承(后续继承章节详解)。
cpp 复制代码
class Time
{
    friend class Date; // 声明Date为Time的友元类:Date的所有成员可访问Time的私有成员
public:
    Time(int hour = 0, int minute = 0, int second = 0)
        : _hour(hour)
        , _minute(minute)
        , _second(second)
    {}
private:
    int _hour;
    int _minute;
    int _second;
};

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

    void SetTimeOfDate(int hour, int minute, int second)
    {
        // 直接访问Time的私有成员(因Date是Time的友元类)
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

4. 内部类

概念

一个类定义在另一个类的内部,称为内部类(嵌套类)。内部类是独立的类,不属于外部类,外部类也无特殊访问权限,但内部类天生是外部类的友元。

核心特性

  1. 内部类可定义在外部类的public/protected/private区域,访问权限受对应限定符限制;
  2. 内部类可直接访问外部类的静态成员(无需外部类对象 / 类名);
  3. 内部类访问外部类的非静态成员,需通过外部类的对象参数(因无this指针);
  4. sizeof(外部类) 仅计算外部类自身成员大小,与内部类无关(内部类独立存储)。
cpp 复制代码
class A
{
private:
    static int k; // 静态成员
    int h;        // 非静态成员
public:
    class B // 内部类:天生是A的友元
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl;    // 直接访问A的静态成员
            cout << a.h << endl; // 通过A的对象访问非静态成员
        }
    };
};

int A::k = 1; // 外部类静态成员初始化

int main()
{
    A::B b; // 访问内部类:外部类名::内部类名
    b.foo(A()); // 传递外部类匿名对象,供内部类访问非静态成员
    return 0;
}

5. 匿名对象

概念

无对象名的临时对象,语法:类名(参数)。生命周期仅当前行(行结束后自动调用析构函数),适用于仅需单次调用成员函数的场景。

cpp 复制代码
class A
{
public:
    A(int a = 0) : _a(a)
    {
        cout << "A(int a)" << endl;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};

class Solution {
public:
    int Sum_Solution(int n) {
        return n + (n > 1 ? Sum_Solution(n - 1) : 0);
    }
};

int main()
{
    A aa1; // 普通对象:生命周期到main函数结束
    A();   // 匿名对象:生命周期仅当前行(输出A(int a) → ~A())
    A aa2(2); // 普通对象

    // 匿名对象的实用场景:单次调用成员函数
    Solution().Sum_Solution(10); // 无需定义Solution对象,直接调用函数
    return 0;
}

!warning\] 注意避免误写为`A aa1();`(编译器会解析为函数声明,而非对象创建),匿名对象直接写`A();`即可。

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

编译器在传参、传返回值过程中,会对连续的构造 + 拷贝构造操作进行优化,减少对象拷贝次数(不同编译器优化力度可能不同,以 g++/VS 为主)。

优化示例(基于以下类)

cpp 复制代码
class A
{
public:
    A(int a = 0) : _a(a)
    {
        cout << "A(int a)" << endl; // 构造函数
    }
    A(const A& aa) : _a(aa._a)
    {
        cout << "A(const A& aa)" << endl; // 拷贝构造函数
    }
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl; // 赋值重载
        if (this != &aa)
            _a = aa._a;
        return *this;
    }
    ~A()
    {
        cout << "~A()" << endl; // 析构函数
    }
private:
    int _a;
};

不同场景的优化效果

场景 1:传值传参(无优化)
cpp 复制代码
void f1(A aa) {} // 传值:拷贝构造aa

int main()
{
    A aa1; // 构造:A(int a)
    f1(aa1); // 拷贝构造:A(const A& aa)
    // 析构顺序:aa(f1参数)→ aa1
    return 0;
}

输出顺序:A (int a) → A (const A& aa) → ~A () → ~A ()

场景 2:传值返回(优化为直接构造)
cpp 复制代码
A f2()
{
    A aa; // 构造:A(int a)
    return aa; // 若不优化:拷贝构造临时对象;优化后:直接构造临时对象
}

int main()
{
    f2(); // 优化后输出:A(int a) → ~A()
    return 0;
}

优化效果:连续构造 + 拷贝构造 → 直接构造(减少 1 次拷贝)

场景 3:隐式类型转换(优化为直接构造)
cpp 复制代码
void f1(A aa) {}

int main()
{
    f1(1); // 1→临时A对象(构造)→ 传参(拷贝构造);优化后:直接构造aa
    // 优化后输出:A(int a) → ~A()
    return 0;
}

优化效果:隐式构造 + 拷贝构造 → 直接构造(减少 1 次拷贝)

场景 4:表达式内连续拷贝(优化为 1 次拷贝)
cpp 复制代码
int main()
{
    A aa2 = f2(); // f2返回临时对象(拷贝构造)→ aa2(拷贝构造);优化后:1次拷贝
    // 优化后输出:A(int a) → ~A()
    return 0;
}

优化效果:连续拷贝构造 + 拷贝构造 → 1 次拷贝构造(减少 1 次拷贝)

场景 5:拷贝构造 + 赋值重载(无优化)
cpp 复制代码
int main()
{
    A aa1;
    aa1 = f2(); // f2返回临时对象(拷贝构造)→ 赋值给aa1(赋值重载);无优化
    // 输出:A(int a) → A(int a) → A& operator=(...) → ~A() → ~A()
    return 0;
}

结论:赋值重载无法优化,仅连续构造 / 拷贝构造可优化。

7. 再次理解封装

计算机无法直接识别现实世界的实体,需通过 "抽象→描述→实例化" 的流程,将实体转化为计算机可识别的对象:

  1. 抽象:对现实实体的属性(如洗衣机的容量、品牌)和行为(洗衣、甩干)进行提炼;
  2. 描述:用面向对象语言(C++)将抽象结果定义为 "类"(自定义类型);
  3. 实例化:通过类创建具体对象,计算机即可通过对象模拟现实实体的行为。
现实世界 计算机世界 核心动作
实体(洗衣机、人) 对象 实例化
实体的抽象特征 类(自定义类型) 描述

核心本质:类是对一类实体的 "模板化描述",对象是模板的 "具体实例",封装则是将实体的属性和行为绑定在类中,通过访问限定符控制外部交互接口。

相关推荐
浩瀚地学6 小时前
【Java】面向对象进阶-接口
java·开发语言·经验分享·笔记·学习
cpp_25016 小时前
P1583 魔法照片
数据结构·c++·算法·题解·洛谷
fpcc6 小时前
跟我学C++中级篇——constinit避免SIOF
c++
无限进步_6 小时前
【C语言】堆排序:从堆构建到高效排序的完整解析
c语言·开发语言·数据结构·c++·后端·算法·visual studio
雾岛听蓝6 小时前
STL 容器适配器:stack、queue 与 priority_queue
开发语言·c++
vortex56 小时前
AppArmor 受限 Shell 环境绕过技术分析:利用动态链接器路径差异实现 Profile 逃逸
linux·运维·服务器·网络安全
CSDN_RTKLIB6 小时前
【One Definition Rule】多编译单元定义同名全局变量
开发语言·c++
春日见7 小时前
python3语法学习
linux·运维·服务器·人工智能·驱动开发
码界奇点7 小时前
前端基础知识构建现代Web应用的基石
前端·青少年编程·web
天寒心亦热7 小时前
Ubuntu20.04系统WIFI网络监测及自动重启
linux·运维·服务器