【C++ 基础入门】类和对象(下):初始化列表、static 成员、友元与内部类

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《C++学习》

📑 目录


一、前置讲解

在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。

🧠 前置知识一:初始化 vs 赋值

初始化只有一次 (对象诞生时),赋值可以多次 ------构造函数体里的语句严格说是赋初值。

📋 前置知识二:初始化列表

构造函数体前的 : 成员(值), ...,是真正初始化成员 的地方,顺序由声明顺序决定。

🔢 前置知识三:static 成员

属于类而不属于对象 ,所有对象共享一份 ;变量要类外定义 ,函数没有 this。

🤝 前置知识四:友元(friend)

让类外的函数或类 拿到访问私有成员的权限,是突破封装 的手段,用多了会增加耦合。

🧩 前置知识五:内部类

定义在类内部 的类,是独立的类 ,只受作用域 约束;能访问外部类成员,但外部的私有访问它没有。


二、再谈构造函数

2.1 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值:

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.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;
};

四条要点:

序号 要点
1 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2 引用成员变量 、const 成员变量 、没有默认构造函数的自定义类型成员 ,必须放在初始化列表位置初始化
3 尽量使用初始化列表 :因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先走初始化列表
4 成员变量在类中的声明次序,就是它在初始化列表中的初始化顺序 ,与它在初始化列表里的书写先后无关

第 1=2 条的例子 (A 没有默认构造,_ref 是引用,_n 是 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
};

第 4 条的例子 (声明顺序是 _a2 在前、_a1 在后,所以先初始化 _a2):

cpp 复制代码
class A
{
public:
    A(int a)
        : _a1(a)
        , _a2(_a1)
    {}
​
    void Print()
    {
        cout << _a1 << " " << _a2 << endl;   // 打印结果 1 随机值
    }
private:
    int _a2;   // 先声明
    int _a1;   // 后声明
};
​
int main()
{
    A aa(1);
    aa.Print();
}

运行结果:

复制代码
1 随机值

💡 注意初始化列表里写的是 _a1(a), _a2(_a1),看起来 _a2 拿到的是 _a1,但实际顺序按声明来 :先初始化 _a2,此时 _a1 还没初始化 (是随机值),所以 _a2 拿到随机值。这就是"声明顺序决定初始化顺序 "的经典坑------初始化列表的书写顺序骗不了编译器。

2.3 explicit 关键字

构造函数不仅能构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数有三种表现:

序号 形式
1 构造函数只有一个参数
2 构造函数有多个参数 ,除第一个参数没有默认值外,其余参数都有默认值
3 全缺省构造函数

格式:explicit 函数

不加 explicit 时(允许隐式类型转换):

cpp 复制代码
class Date
{
public:
    // 单参构造函数,没有 explicit 修饰,具有类型转换作用
    Date(int year)
        : _year(year)
    {}
​
private:
    int _year;
    int _month;
    int _day;
};
​
void Test()
{
    Date d1(2022);   // 能过:直接调用构造函数
​
    // 用一个整型变量给日期类型对象赋值
    // 编译器背后会用 2023 构造一个无名对象,再用无名对象给 d1 赋值
    d1 = 2023;       // 能过:触发隐式类型转换
}

加上 explicit 时(禁止隐式类型转换):

cpp 复制代码
class Date
{
public:
    explicit Date(int year)   // explicit 禁止单参构造函数的隐式转换
        : _year(year)
    {}
​
private:
    int _year;
    int _month;
    int _day;
};
​
void Test()
{
    Date d1(2022);   // 能过
    d1 = 2023;       // 不能过:编译报错
}

运行结果:

复制代码
d1 = 2023; 编译报错:无法从 "int" 转换为 "Date"

用 explicit 修饰构造函数,将会禁止构造函数的隐式转换功能。

💡 什么时候加 explicit?单参构造函数建议默认加 ------否则 Date d = 2023; 这种写法会"悄悄"通过,读代码的人根本看不出这里发生了一次构造。


三、static 成员

声明为 static 的类成员 称为类的静态成员 :用 static 修饰的成员变量 叫静态成员变量 ,用 static 修饰的成员函数 叫静态成员函数 。静态成员变量一定要在类外进行初始化。

3.1 特性

序号 特性
1 静态成员为所有类对象所共享 ,不属于某个具体的对象 ,存放在静态区
2 静态成员变量必须在类外定义 ,定义时不添加 static 关键字 ,类中声明
3 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问
4 静态成员函数没有隐藏的 this 指针 ,不能访问任何非静态成员
5 静态成员也是类的成员 ,受 public、protected、private 访问限定符的限制

示例(类内声明,类外定义):

cpp 复制代码
class Student
{
public:
    Student()
    {
        ++_count;
    }

    // 静态成员函数:没有 this 指针,只能访问静态成员
    static int GetCount()
    {
        return _count;
    }

private:
    static int _count;   // 类中只是【声明】
    int _age;            // 非静态成员
};

// 类外定义并初始化(定义时不加 static)
int Student::_count = 0;

运行结果(创建 3 个对象后):

复制代码
Student::GetCount() = 3

💡 C++17 起有个例外 :用 inline static int _count = 0; 可以在类内直接定义并初始化,不用再写类外定义。老标准下则必须类外定义------因为类内声明不分配存储空间,类外定义才真正分配。

3.2 两个思考题

问题 能否 原因
1. 静态成员函数可以调用非静态成员函数吗? 不可 静态函数没有 this 指针,不知道要操作哪个对象
2. 非静态成员函数可以调用类的静态成员函数吗? 可以 静态成员属于类,任何成员函数都能访问

💡 一句话记:静态函数是"无状态"的,它只能碰"全类共享"的东西;而非静态函数既有对象也有类,两头都能碰。


四、友元

友元提供了一种突破封装 的方式,虽然有时能带来便利,但也会增加类之间的耦合度、破坏封装性 ,因此不宜过度使用。

友元主要分为两种:友元函数 和友元类。

4.1 友元函数

友元解决的一个重要问题是什么?

当我们尝试重载 operator<<(输出运算符)时,会发现无法将其重载为成员函数 ------因为 cout 输出流对象与隐含的 this 指针会争夺第一个参数的位置 。成员函数的第一个参数默认为隐藏的 this 指针,这意味着左操作数必须是对象本身 ;而实际使用中,cout 需要作为第一个形参 才能正常用。因此需要把 operator<< 重载为全局函数 ,但这又导致无法访问类的私有成员 ------此时友元机制 便提供了解决方案。operator>>(输入运算符)情况类似。

反例:重载成成员函数(不符合调用习惯):

cpp 复制代码
class Date
{
public:
    Date(int year, int month, int day)
        : _year(year), _month(month), _day(day)
    {}

    // d1 << cout; 会转换为 d1.operator<<(&d1, cout),不符合常规调用习惯
    // 因为成员函数的第一个参数一定是隐藏的 this,所以 d1 必须放在 << 的左侧
    ostream& operator<<(ostream& _cout)
    {
        _cout << _year << "-" << _month << "-" << _day << endl;
        return _cout;
    }

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

正解:声明为友元函数,重载成全局函数:

友元函数 可以直接访问类的私有成员,它是一个定义在类外部的普通函数 ,不属于任何类 。需要在类的内部进行声明 ,声明时加上 friend 关键字。

格式:friend 函数

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;
    _cin >> d._month;
    _cin >> d._day;
    return _cin;
}

int main()
{
    Date d(2022, 1, 13);
    cout << d << endl;   // 正常用法:cout 在左,日期在右
    return 0;
}

运行结果:

复制代码
2022-1-13

说明:

序号 说明
1 友元函数可以访问类的私有和保护成员 ,但不是类的成员函数
2 友元函数不能用 const 修饰
3 友元函数可以在类定义的任何地方声明 ,不受类访问限定符限制
4 一个函数可以是多个类的友元函数
5 友元函数的调用方式与普通函数相同

4.2 友元类

友元类的所有成员函数都可以作为另一个类的友元函数 ,从而访问另一个类中的非公有成员。

三条限制:

限制 说明
单向、不具交换性 若 Time 类声明 Date 类为其友元类,则 Date 可以直接访问 Time 的私有成员,但 Time 无法访问 Date 的私有成员
不能传递 如果 B 是 A 的友元、C 是 B 的友元,推不出 C 是 A 的友元
不能继承 友元关系不随继承传递(继承部分再细讲)

💡 把友元关系记成"单方面的通行证 ":它只对发证的一方有效,不能转借 (不传递),也不能传给孩子(不继承)。


五、内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类 ,它不属于外部类 ,更不能通过外部类的对象 去访问内部类的成员 。外部类对内部类没有任何越界的访问权限。

注意 :内部类是外部类的"天然友元" ------内部类可以通过外部类的对象参数 来访问外部类中的所有成员 (包括私有)。但反过来,内部类不是外部类的友元 ,外部类不能访问内部类的私有成员。

特性:

序号 特性
1 内部类可以定义在外部类的 public、protected、private 区域,都是可以的
2 内部类可以直接访问外部类中的 static 成员 ,不需要外部类的对象或类名
3 sizeof(外部类) 只算外部类自己的成员 ,和内部类没有任何关系
cpp 复制代码
class A
{
private:
    static int k;
    int h;
public:
    class B   // B 天生是 A 的友元,可访问 A 的私有成员
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl;      // OK:直接访问外部类的 static 成员
            cout << a.h << endl;    // OK:通过外部类对象访问非静态成员
        }
    };
};

int A::k = 1;

int main()
{
    A::B b;
    b.foo(A());
    return 0;
}

运行结果:

复制代码
1
(h 未初始化,输出随机值)

💡 第三条最好用 sizeof 验证:sizeof(A) 只等于 h 和 k 相关的大小,内部类 B 哪怕有一堆成员,也不会计入外部类的大小 ------因为 B 只是类型定义,不占外部类对象的内存。


六、再次理解类与对象

类是对某一类实体(对象)来进行描述的 :描述该对象具有哪些属性 、哪些方法 ,描述完成后就形成了一种新的自定义类型 ;再用该自定义类型去实例化具体的对象。

概念 是什么 占内存吗
类 自定义类型 ,是图纸/蓝图 不占(定义不分配空间)
对象 该类类型的一个具体实例 ,是房子 占(有独立空间)

大家要重点理解类与对象的关系:类是抽象的,对象是具体的;类是对象的蓝图,对象是类的实体化。


七、复盘(附答案)

💡 思考题

  1. 初始化列表 :初始化列表和函数体内赋值的本质区别 是什么?哪些成员必须 在初始化列表中初始化?初始化列表的初始化顺序由什么决定,和书写顺序有关吗?

  2. static 成员 :静态成员变量为什么必须在类外单独定义 ?静态成员函数为什么不能访问非静态成员 ?静态成员变量能否在类内初始化(什么情况下可以)?

  3. 友元 :友元函数破坏了封装吗 ?为什么实际开发中要谨慎使用友元 ?友元关系是单向还是双向 ?能否传递?

  4. 内部类 :内部类和外部类的关系是什么?内部类能否直接访问外部类的非静态成员 ?内部类的访问权限受外部类访问限定符的影响吗?

📝 答案

  1. 初始化列表是直接构造 成员变量(只调用一次构造函数),赋值是先构造再赋值 (调用默认构造 + 赋值运算符)。const 成员、引用成员、没有默认构造函数的类成员 必须使用初始化列表。初始化顺序由成员声明的顺序 决定,与初始化列表的书写顺序无关。

  2. 静态成员变量属于类、所有对象共享一份 ,必须在类外单独定义以分配存储空间 (类内只是声明)。静态成员函数没有 this 指针 ,因此不能访问非静态成员 。C++17 起 可以用 inline static 在类内初始化;老标准下不行,必须类外定义。

  3. 友元破坏了封装 ,因为可以访问私有成员。实际开发中应谨慎使用 ,仅在必要时(如运算符重载 << / >>)使用。友元关系是单向 的(A 是 B 的友元不代表 B 是 A 的友元),不能传递 (A 是 B 的友元、B 是 C 的友元,不代表 A 是 C 的友元),也不能继承。

  4. 内部类独立于外部类存在 ,不依赖外部类对象。内部类不能直接访问外部类的非静态成员 (因为没有外部类对象),但可以直接访问外部类的 static 成员 ;要访问非静态成员,需要传入外部类对象 。内部类的访问权限受外部类访问限定符影响 (例如 Inner 声明在 private 区,外部就无法使用 Outer::Inner 这个类型)。


结语

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

相关推荐
j7~24 分钟前
【C++ 八股文】(篇四)C++11 新特性
开发语言·c++·面试·c++11·八股文
LONGZHIQIN32 分钟前
STM32学习笔记
笔记·stm32·学习·arm
eggwyw38 分钟前
C++中大括号的用法合集
开发语言·c++
xiaoye-duck40 分钟前
《C++ 项目:手写大模型接入 SDK》本地 Ollama 部署与适配
开发语言·网络·c++·语言模型
叼烟扛炮1 小时前
[LangChain] 学习笔记(04):手搓 ChatGPT 打字机——流式输出(SSE)与模型初始化选型
人工智能·笔记·chatgpt·langchain
redtank20001 小时前
纯血鸿蒙小工具三件套实测:弹幕手持、爱记录与仓库管理软件
学习·华为·智能手机·harmonyos
§ꦿCFོ༉1 小时前
嵌入式八股文—C语言入门学习(二)
c语言·学习·嵌入式·编程语言
octopus_c1 小时前
STL 之 string:auto 与范围 for、常用接口、模拟实现与深浅拷贝
开发语言·c++·算法
李帅朋1 小时前
INP-Former阅读笔记
笔记·深度学习·计算机视觉