类和对象(下):初始化列表、静态成员与友元深度解析

/-------------C++初阶-------------/

《 C++发展史、命名空间、输入输出、缺省参数、函数重载 》

《 C++引用、内联函数、auto、范围 for、nullptr 》

《 类和对象(上篇)》

《 类和对象(中篇)》

《 C++const成员与日期类 》
在 C++ 面向对象编程中,类和对象是核心基石。除了基础的类定义与对象创建,构造函数的进阶用法、静态成员的共享机制、友元的封装突破以及内部类的设计技巧,直接决定了代码的优雅度与效率。本文将结合实例,深入拆解这些关键知识点,帮你彻底掌握类和对象的进阶应用。

🚀 个人主页< 脏脏a-CSDN博客 >

📊 文章专栏:< C++ >

📋 其他专栏:< Linux > 、<数据结构 > 、<优选算法>

目录

[一、再谈构造函数: 初始化列表](#一、再谈构造函数: 初始化列表)

[1. 构造函数体赋值 vs 初始化列表](#1. 构造函数体赋值 vs 初始化列表)

[2. 必须使用初始化列表的场景](#2. 必须使用初始化列表的场景)

[3. 初始化列表的关键注意事项](#3. 初始化列表的关键注意事项)

[4. explicit 关键字:禁止隐式类型转换](#4. explicit 关键字:禁止隐式类型转换)

5、坑

[二、static 成员](#二、static 成员)

[【 静态成员的核心特性】](#【 静态成员的核心特性】)

【面试题】:实现一个类,计算程序中创建出了多少个类对象。

【小彩蛋】

三、友元

[1. 友元函数](#1. 友元函数)

[2. 友元类](#2. 友元类)

四、内部类

[1. 内部类的核心特性](#1. 内部类的核心特性)

[2. 代码示例](#2. 代码示例)

五、对象拷贝时编译器的优化

【小彩蛋】:匿名对象

六、再次理解封装


一、再谈构造函数: 初始化列表

1. 构造函数体赋值 vs 初始化列表

创建对象时,构造函数体中的语句本质是 "赋初值" 而非 "初始化"------ 因为初始化只能执行一次,而赋值可多次进行。

初始化列表则是真正的初始化场景,它以冒号开头,用逗号分隔成员变量,格式为**成员变量(初始值)**。例如:

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

private:
    int _year;
    int _month;
    int _day;
};

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)
     , _ref(ref)
     , _n(10) 
    {}

private:
    A _aobj;      // 无默认构造函数的自定义类型
    int& _ref;    // 引用成员
    const int _n; // const成员
};

3. 初始化列表的关键注意事项

成员变量的初始化顺序由类中声明顺序 决定,与初始化列表中的顺序无关。例如:

cpp 复制代码
class A 
{
public:
    A(int a) 
     : _a1(a)
     , _a2(_a1) 
    {} // 声明顺序是_a2在前,_a1在后

    void Print() { cout << _a1 << " " << _a2 << endl; }

private:
    int _a2; // 先声明
    int _a1; // 后声明
};

int main()
{
    A a(1);
    a.printf();
    return 0;
}
// 输出:1 随机值(_a2先初始化时,_a1尚未赋值)

无论是否显式使用初始化列表,自定义类型成员都会先通过初始化列表完成初始化。

4. explicit 关键字:禁止隐式类型转换

接收单个参数的构造函数(含全缺省构造函数、仅第一个参数无默认值的多参数构造函数),默认支持隐式类型转换。例如:

cpp 复制代码
class Date {
public:
    Date(int year)
        : _year(year) 
    {} // 单参构造函数

private:
    int _year;
};

// 隐式转换:用2023构造无名对象,再赋值给d1
Date d1 = 2023;

使用**explicit修饰构造函数**,可禁止这种隐式转换,提升代码可读性:

cpp 复制代码
class Date 
{
public:
    explicit Date(int year) 
                 : _year(year) 
    {} // 禁止隐式转换
private:
    int _year;
};

// 编译失败:无法进行隐式类型转换
Date d1 = 2023;

5、坑

由于用2构造出来的对象是临时的,所以想要用引用绑定的话得用const引用,const引用会延长临时变量的生命周期,直到这个引用变量销毁

**【注意】:**初始化列表和函数体内赋值并不冲突,初始化列表能处理很多函数体内赋值处理不了的场景,他两之间是先执行初始化列表,再执行函数体内赋值

二、static 成员

static 成员是属于整个类的共享资源,而非某个具体对象,常用于实现类级别的统计、共享配置等功能。

【 静态成员的核心特性】

  • 静态成员变量: 需在类内声明、类外初始化(初始化时不加 static 关键字),存储在静态区;
  • **静态成员函数:**无隐藏的 this 指针,只能访问静态成员,不能访问非静态成员;
  • 访问方式:支持类名::静态成员对象.静态成员两种访问方式;
  • **访问控制:**受 public、protected、private 限定符约束,private 静态成员仅类内可访问。

【面试题】:实现一个类,计算程序中创建出了多少个类对象。

通过静态成员变量统计对象创建与销毁的次数,是面试高频考点:

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; // 类外初始化

// 测试代码
void TestA() {
    cout << A::GetACount() << endl; // 输出:0(无对象)
    A a1, a2;
    A a3(a1); // 拷贝构造
    // 输出:3(3个对象存在)
    cout << A::GetACount() << endl;
}

【小彩蛋】

1、静态成员函数能否调用非静态成员函数?

答:不能,因为无 this 指针,无法访问具体对象的非静态成员;

2、非静态成员函数能否调用静态成员函数?

答: 可以,静态成员属于类,同时也不依赖对象,可以直接访问。

三、友元

友元提供了一种访问类私有成员的方式,虽能提升灵活性,但会破坏封装、增加耦合度(描述不同模块之间的依赖关联程度),需谨慎使用。友元分为友元函数和友元类。

1. 友元函数

**重载operator<<和operator>>**时,因 cout/cin 需作为左操作数,无法重载为成员函数(成员函数默认 this 指针为第一个参数)。此时友元函数可解决类外访问私有成员的问题:

cpp 复制代码
class Date 
{
    // 声明友元函数,允许其访问私有成员
    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;
};

// 友元函数实现:直接访问私有成员
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;  // 输入:2024 5 20
    cout << d; // 输出:2024-5-20
    return 0;
}

operator<<必须传const引用,因为临时对象是右值(具有常性),非const引用无法绑定右值;若用非const引用,传入临时对象时会触发编译错误,这是C++对右值不可修改的规则限制。

【注意】:

右值和临时对象具有常性就是因为避免 "修改一个马上要销毁的对象" 这种无意义且危险的操作
友元函数的核心特点:

  • 不是类的成员函数,无 this 指针;
  • 可访问类的公有/ 私有 / 保护(非静态成员需要用对象去访问)成员,但不能用 const 修饰;
  • 可在类内任意位置声明,不受访问限定符限制;
  • 一个函数可作为多个类的友元。

2. 友元类

友元类的所有成员函数,都可访问另一个类的非公有成员。但友元关系是单向且不可传递的:

cpp 复制代码
class Time 
{
    friend class Date; // 声明Date为Time的友元类
public:
    Time(int hour = 0) : _hour(hour) {}
private:
    int _hour;
};

class Date 
{
public:
    void SetTime(int hour)
    {
        // 直接访问Time的私有成员
        _t._hour = hour;
    }
private:
    Time _t;
};

注意事项:

  • 单向性: Date 可访问 Time 的所有成员(必须有Time对象才能访问非静态成员),但 Time 不能访问 Date 的所有成员;
  • **不可传递:**若 C 是 B 的友元,B 是 A 的友元,C 不一定是 A 的友元;
  • **不可继承:**友元关系不能被子类继承。

四、内部类

内部类是定义在另一个类内部的类,它是独立的类,不属于外部类,但天生是外部类的友元。

1. 内部类的核心特性

  • 访问权限: 内部类是外部类的友元,外部类只能访问内部类的公有成员;
  • **静态成员访问:**内部类可直接访问外部类的 static 成员,无需外部类对象或类名;
  • 内存大小: sizeof(外部类)仅计算外部类成员,与内部类无关;
  • 访问方式: 需通过**外部类::内部类**的方式创建对象。

2. 代码示例

cpp 复制代码
class A {
private:
    static int k;
    int h; // 私有成员
public:
    class B { // 内部类,天生是A的友元
    public:
        void foo(const A& a) {
            cout << k << endl;  // 直接访问A的static成员
            cout << a.h << endl;// 通过A对象访问私有成员
        }
    };
};

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

int main() {
    A::B b; // 必须通过外部类::内部类创建对象
    b.foo(A()); // 输出:1 和 随机值(A的h未初始化)
    return 0;
}

五、对象拷贝时编译器的优化

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

    A(const A& aa)
         
    {
        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;
};

void Func1(A aa)
{

}

A Func5()
{
    A aa;
    return aa;
}

int main()
{
    A ra1 = Func5(); // 拷贝构造+拷贝构造 ->优化为构造
    cout << "==============" << endl;
    A ra2;
    ra2 = Func5(); //不会优化

    A aa1;
    Func1(aa1); // 不会优化

    Func1(A(1)); // 构造+拷贝构造 ->优化为构造
    Func1(1);    // 构造+拷贝构造 ->优化为构造

    A aa2 = 1;  // 构造+拷贝构造 ->优化为构造


    return 0;
}

编译器对拷贝构造的优化(比如返回值优化、复制消除),更大概率发生在"对象的构造与使用处于同一表达式(可粗略理解为"同一行代码")"的场景中;而如果拆分到多行(多个表达式),优化往往难以生效,拷贝构造函数大概率会被实际调用。

【小彩蛋】:匿名对象

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

匿名对象**⽣命周期只在当前⼀⾏**,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。

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) {
        cout << "Sum_Solution" << endl;
        //...
        return n;
    }
};


void push_back(const string & s)
{
    cout << "push_back:" << s << endl;
}

int main()
{
    A aa(1);  // 有名对象 -- 生命周期在当前函数局部域
    A(2);     // 匿名对象 -- 生命周期在当前行
     

    Solution sl;
    sl.Sum_Solution(10);

    Solution().Sum_Solution(20);

    //A& ra = A(1);  // 匿名对象具有常性
    const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域
    
    return 0;
}

六、再次理解封装

面向对象的核心是封装、继承、多态,而封装的本质是 "将现实实体抽象为代码中的类"

  1. **现实世界:**存在各种实体(如洗衣机、日期);
  2. **抽象过程:**提炼实体的属性(如洗衣机的容量、日期的年 / 月 / 日)和行为(如洗衣机的洗衣功能、日期的输出功能);
  3. **代码实现:**用类描述抽象结果,形成自定义类型;
  4. **实例化:**通过类创建对象,让计算机 "认识" 现实实体。

类是对实体的抽象描述,对象是抽象的具体实例。封装则是通过访问限定符(public/private/protected),隐藏内部实现细节,仅暴露必要的接口,保证数据安全。

相关推荐
white-persist2 小时前
二进制movl及CTF逆向GDB解析:Python(env)环境下dbg从原理到实战
linux·服务器·开发语言·python·网络安全·信息可视化·系统安全
lkbhua莱克瓦242 小时前
Java进阶——集合进阶(MAP)
java·开发语言·笔记·github·学习方法·map
代码狂想家2 小时前
Rust 命令行密码管理器工具开发
开发语言·rust·php
u0119608232 小时前
java 不可变集合讲解
java·开发语言
Dream it possible!2 小时前
LeetCode 面试经典 150_二叉树_二叉树中的最大路径和(77_124_C++_困难)(DFS)
c++·leetcode·面试·二叉树
翔云 OCR API2 小时前
NFC护照鉴伪查验流程解析-ICAO9303护照真伪查验接口技术方案
开发语言·人工智能·python·计算机视觉·ocr
2501_941111682 小时前
模板编译期哈希计算
开发语言·c++·算法
Creeper.exe2 小时前
【C语言】分支与循环(上)
c语言·开发语言