C++4(类与对象下篇)

作为 C++ 初学者,在掌握了类和对象的基础用法后,下篇的知识点会显得 "有点绕"------ 初始化列表、static、友元这些概念总让人分不清,今天就用能看懂的语言,把这些知识点掰开揉碎,学完既能理解又能上手写代码,复习也一目了然~
📚 超贴心目录
1.🚦 构造函数进阶:别再把 "赋初值" 当 "初始化" 啦
1.1 构造函数体赋值:为啥说它不是真・初始化?
1.2 初始化列表:新手必避的坑都在这
1.3 explicit 关键字:禁止 "偷偷" 的类型转换
2.⚡ static 成员:所有对象 "共享" 的专属道具
2.1 静态成员变量:类内声明、类外初始化是铁律
2.2 静态成员函数:没有 this 指针的 "特殊选手"
2.3 新手必看:静态成员常见问题解答
3.🤝 友元:突破封装的 "特殊朋友"(慎用!)
3.1 友元函数:为啥重载 <<和>> 必须用它?
3.2 友元类:单向的 "好友权限"
4.📦 内部类:藏在别人家里的 "独立打工人"
4.1 内部类的核心特性:新手别搞混这几点
4.2 实战示例:一看就懂的内部类用法
💡 再聊封装:从现实世界看懂 C++ 的核心思想
📝 专属练习:边练边巩固,不怕忘

1. 🚦 构造函数进阶:别再把 "赋初值" 当 "初始化" 啦

对于初学者来说,构造函数最开始学的是 "给成员变量赋值",但其实这里藏着一个容易混淆的点:构造函数体里的赋值,不是真正的初始化。
1.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;
};

为啥说这不是 "初始化"?

初始化:变量创建时只赋一次值,是 "从无到有" 的过程;

赋初值:变量已经创建(默认初始化了),只是修改它的值,是 "从有到改" 的过程。

简单记:构造函数体里的代码,是对象创建好之后才执行的,所以只能改值,不能算 "初始化"

1.2 初始化列表:必避的坑都在这

既然构造函数体赋值不是真初始化,那 C++ 里 "真正的初始化" 靠啥?------初始化列表。

对初学者来说,记住语法和规则,就能少踩 90% 的坑。

cpp 复制代码
class Date
{
public:
    // 初始化列表:冒号开头,成员变量用括号赋值
    Date(int year, int month, int day)
        : _year(year)   // 这才是真正的初始化
        , _month(month)
        , _day(day)
    {}  // 函数体可以空着
private:
    int _year;
    int _month;
    int _day;
};

必记的 4 条规则(记牢不踩坑)
1.必须用初始化列表的 3 种情况(最容易忘!):
引用成员变量 :引用必须在创建时初始化,没法先创建再赋值;

const 成员变量 :const 变量一旦创建就不能改,必须初始化;
自定义类型成员(且该类无默认构造函数) :比如 A 类只有带参数的构造函数,那么包含 A 类的 B 类,必须用初始化列表给 A 类成员赋值。
友好示例

cpp 复制代码
// 自定义类A:只有带参数的构造函数
class A
{
public:
    A(int a) : _a(a) {} // 没有默认构造函数
private:
    int _a;
};

// 类B包含A类成员、引用、const
class B
{
public:
    // 新手注意:这3个成员必须用初始化列表初始化
    B(int a, int ref)
        : _aobj(a)  // A类无默认构造,必须初始化
        , _ref(ref) // 引用必须初始化
        , _n(10)    // const必须初始化
    {}
private:
    A _aobj;    // 自定义类型成员
    int& _ref;  // 引用成员
    const int _n; // const成员
};

2.初始化顺序≠书写顺序 (高频坑!):
成员变量的初始化顺序,只看它在类里 "声明的顺序",和初始化列表里写的先后无关

cpp 复制代码
class A
{
public:
    // 新手以为:先初始化_a1,再用_a1初始化_a2
    A(int a) : _a1(a), _a2(_a1) {}
    void Print() { cout << _a1 << " " << _a2 << endl; }
private:
    int _a2; // 先声明:先初始化_a2
    int _a1; // 后声明:后初始化_a1
};

int main()
{
    A aa(1);
    aa.Print(); // 输出:1 随机值!
    // 原因:初始化_a2时,_a1还没初始化,所以_a2是随机值
}

避坑:写初始化列表时,按成员变量在类里的声明顺序写,就不会错!

  1. 所有成员都会走初始化列表:
    哪怕你没写初始化列表,编译器也会自动帮你走一遍 ------ 比如自定义类型成员,会先调用它的默认构造函数。所以新手尽量都用初始化列表,代码更高效
    4.每个成员只能初始化一次:

初始化列表里一个成员只能出现一次,符合 "初始化只做一次" 的规则

1.3 explicit 关键字:禁止 "偷偷" 的类型转换

cpp 复制代码
class Date
{
public:
    Date(int year) : _year(year) {}
private:
    int _year = 1900;
};

int main()
{
    Date d1(2024);
    d1 = 2025; // 新手疑惑:为啥int能直接赋值给Date对象?
    return 0;
}

这是因为单参数构造函数默认支持隐式类型转换 :编译器会先用 2025 创建一个临时的 Date 对象,再把这个临时对象赋值给 d1。这种 "偷偷" 的转换,容易让代码出问题,也不利于新手理解

explicit 的作用:禁止隐式转换

只需要记住:在单参数构造函数前加explicit,就能阻止这种隐式转换。

cpp 复制代码
class Date
{
public:
    explicit Date(int year) : _year(year) {} // 加explicit
private:
    int _year = 1900;
};

int main()
{
    Date d1(2024);
    // d1 = 2025; // 编译报错!新手再也不用担心误写
    return 0;
}

补充:多参数构造函数如果除了第一个参数,其他都有默认值,也会触发隐式转换,同样可以用explicit禁止。

2. ⚡ static 成员:所有对象 "共享" 的专属道具

新手可以把 static 成员理解为:属于 "整个类" 的道具,不是某个对象的 ------ 不管创建多少个对象,这个道具只有一份,所有对象都能用。
2.1 静态成员变量:类内声明、类外初始化是铁律

最容易犯的错:只在类里声明静态成员变量,忘了类外初始化。

cpp 复制代码
class Student
{
public:
    Student(string name) : _name(name)
    {
        _totalCount++; // 每创建一个学生,总数+1
    }
private:
    string _name; // 每个学生的专属名字(非静态)
    static int _totalCount; // 静态成员:所有学生的总数(类内声明)
};

// 静态成员变量:类外初始化(必写!)
int Student::_totalCount = 0; // 初始化时不加static

理解:_totalCount是所有 Student 对象共享的,哪怕创建 100 个学生,_totalCount只有一份,值会跟着对象创建累加。

2.2 静态成员函数:没有 this 指针的 "特殊选手"

静态成员函数是为了操作静态成员变量而生的,要记住:它没有隐藏的this指针。.

cpp 复制代码
class Student
{
public:
    Student(string name) : _name(name)
    {
        _totalCount++;
    }
    // 静态成员函数:获取学生总数
    static int GetTotalCount()
    {
        // 能访问静态成员变量
        return _totalCount;
        // 不能访问非静态成员变量(比如_name),因为没有this指针
        // return _name; // 编译报错!
    }
private:
    string _name;
    static int _totalCount;
};

int Student::_totalCount = 0;

int main()
{
    // 新手注意:静态成员函数可以直接用"类名::函数名"调用,不用创建对象
    cout << "初始学生数:" << Student::GetTotalCount() << endl; // 输出0
    Student s1("小明"), s2("小红");
    cout << "当前学生数:" << Student::GetTotalCount() << endl; // 输出2
    return 0;
}

2.3 必看:静态成员常见问题解答

1.静态成员函数能调用非静态成员函数吗?

不能!记:非静态成员函数需要 this 指针,静态成员函数没有 this,所以调不了。

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

能!非静态成员函数有 this 指针,调用静态成员函数时,不用传任何额外参数,直接调就行。

3.静态成员变量可以设置默认值吗?
C++11 及以后,静态成员变量如果是 const 类型,可以在类内直接初始化 (比如static const int _maxNum = 100;),但非 const 的静态成员变量,必须类外初始化。

3. 🤝 友元:突破封装的 "特殊朋友"(慎用!)

C++ 的封装是 "把内部细节藏起来",但有时候需要让外部的函数 / 类访问私有成员 ------ 这时候就需要 "友元"。对新手来说,友元是 "特例",能少用就少用,因为会破坏封装。

3.1 友元函数:为啥重载 <<和>> 必须用它?

学输入输出重载时,会发现一个问题:cout(输出流)要作为第一个参数,但类的成员函数第一个参数是 this 指针,根本没法重载。这时候就要用友元函数。

cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
    // 声明友元函数:加friend关键字,不受访问限定符限制
    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;
};

// 友元函数:全局函数,能直接访问Date的私有成员
ostream& operator<<(ostream& _cout, const Date& d)
{
    _cout << d._year << "年" << d._month << "月" << d._day << "日";
    return _cout; // 支持链式输出(比如cout << d1 << d2)
}

istream& operator>>(istream& _cin, Date& d)
{
    cout << "请输入年、月、日:";
    _cin >> d._year >> d._month >> d._day;
    return _cin; // 支持链式输入(比如cin >> d1 >> d2)
}

int main()
{
    Date d;
    cin >> d;  // 调用重载的>>
    cout << d << endl; // 调用重载的<<
    return 0;
}

总结友元函数的特点:
1.是全局函数,不属于任何类;
2.类内声明加friend,可以写在 public/private 里,都生效;
3.能直接访问私有成员,不用 get/set 函数;
4.调用方式和普通函数一样

3.2 友元类:单向的 "好友权限"

友元类就是 "整个类都是另一个类的朋友",要记住:友元关系是 "单向的、不能传递的"

cpp 复制代码
class Time
{
    // 声明Date是Time的友元类:Date能访问Time的私有成员
    friend class Date;
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:
    void SetTime(int hour, int minute, int second)
    {
        // Date能直接访问Time的私有成员(因为是友元)
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

int main()
{
    Date d;
    d.SetTime(10, 30, 0); // 成功设置Time的私有成员
    // Time不能访问Date的私有成员,比如_year,会编译报错
    return 0;
}

避坑:
单向性:A 是 B 的友元 ≠ B 是 A 的友元;
不可传递:A 是 B 的友元,B 是 C 的友元 ≠ A 是 C 的友元;
友元类的所有成员函数,都能访问被友元类的私有成员

4. 📦 内部类:藏在别人家里的 "独立打工人"

内部类就是 "一个类定义在另一个类里面",容易误以为它是外部类的成员,但其实它是独立的类 ,只是 "住在外部类家里"

4.1 内部类的核心特性

1**.内部类是外部类的 "天生友元"**:能访问外部类的所有成员(私有 / 公有);

2.内部类独立:外部类不能访问内部类的成员,sizeof (外部类) 不包含内部类;

3.访问方式:外部类名::内部类名,比如A::B;

4.内部类可以直接访问外部类的 static 成员,不用外部类的对象 / 类名。

cpp 复制代码
class A
{
private:
    static int _k; // 静态成员
    int _h = 10;   // 非静态成员
public:
    // 内部类B:住在A里面,是A的友元
    class B
    {
    public:
        void ShowA(const A& a)
        {
            // 能访问A的static成员(直接访问)
            cout << "A的静态成员_k:" << _k << endl;
            // 能访问A的非静态成员(需要A的对象)
            cout << "A的非静态成员_h:" << a._h << endl;
        }
        void SetB(int b) { _b = b; } // 内部类自己的成员
    private:
        int _b; // 内部类的私有成员,A不能访问
    };
};

// 外部类的static成员初始化
int A::_k = 20;

int main()
{
    // 新手注意:创建内部类对象的方式
    A::B b;
    A a;
    b.ShowA(a); // 输出:20  10

    // sizeof(A)只算A自己的成员,和B无关
    cout << "sizeof(A):" << sizeof(A) << endl; // 输出4(只算_h)
    return 0;
}

5. 💡 再聊封装:从现实世界看懂 C++ 的核心思想

对我们来说,封装不用记复杂的定义,用现实例子就能懂:

现实世界:你用手机时,只需要点屏幕、按按键(对外的接口),不用知道手机内部的电路板、芯片怎么工作(内部细节);

C++ 世界:类就是 "手机",成员函数是 "按键 / 屏幕(接口)",成员变量是 "电路板(内部细节)",访问限定符(public/private)就是 "保护内部细节不被乱改"。

6. 📝 新手专属练习:边练边巩固,不怕忘

1.统计班级学生人数:用 static 成员变量统计创建了多少个 Student 对象,销毁时计数减 1;

2.日期类完善:给 Date 类加初始化列表,用 explicit 禁止隐式转换,重载 <<输出 "xxxx 年 xx 月 xx 日";

3.友元练习:写一个 Time 类和 Date 类,让 Date 成为 Time 的友元,实现 Date 修改 Time 的小时 / 分钟;

4.内部类练习:在 Person 类里定义内部类 IDCard,实现 IDCard 访问 Person 的姓名和身份证号。

相关推荐
crescent_悦1 小时前
C++:Invert a Binary Tree
开发语言·c++
2401_873204652 小时前
C++与Docker集成开发
开发语言·c++·算法
实心儿儿2 小时前
C++ —— map和set的使用
开发语言·c++
j_xxx404_2 小时前
力扣--分治(归并排序)算法题II:计算右侧小于当前元素的个数,翻转对(无痛通关困难题)
开发语言·数据结构·c++·算法·leetcode
Irissgwe2 小时前
Mysql数据库基础
数据库·c++·mysql·mysql数据库基础
setmoon2142 小时前
多协议网络库设计
开发语言·c++·算法
永远睡不够的入2 小时前
C++继承详解
java·c++·redis
2501_908329852 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
皮卡狮2 小时前
高阶数据结构:红黑树
c++