de风——【从零开始学C++】(四):类和对象(下)


前言

大家好,我是你们的老朋友小小风呀(¯▽¯)/~~!今天我们继续【从零开始学 C++】专题的第四篇,深入学习类和对象的进阶知识。上一篇我们已经了解了类和对象的基础概念,这一篇我们要学习一些更重要的知识点:初始化列表、类型转换、static 成员、友元、内部类、匿名对象、编译器优化

这些内容虽然看起来有点多,但只要跟着我的节奏,用大白话理解,配合代码示例,相信大家一定能轻松掌握!

目录

前言

(一)再探构造函数:初始化列表

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】必须使用初始化列表的场景](#【示例 1】必须使用初始化列表的场景)

[【示例 2】初始化顺序的坑!](#【示例 2】初始化顺序的坑!)

(二)类型转换

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】隐式类型转换演示](#【示例 1】隐式类型转换演示)

[【示例 2】explicit 禁止隐式转换](#【示例 2】explicit 禁止隐式转换)

[(三)static 成员](#(三)static 成员)

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】统计创建了多少个对象](#【示例 1】统计创建了多少个对象)

[【示例 2】静态成员函数没有 this 指针](#【示例 2】静态成员函数没有 this 指针)

(四)友元

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】友元函数](#【示例 1】友元函数)

[【示例 2】友元类](#【示例 2】友元类)

(五)内部类

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】内部类基本用法](#【示例 1】内部类基本用法)

(六)匿名对象

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】匿名对象的生命周期](#【示例 1】匿名对象的生命周期)

(七)对象拷贝时的编译器优化

[1. 基础概念解释](#1. 基础概念解释)

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

[【示例 1】编译器优化完整演示](#【示例 1】编译器优化完整演示)

总结



(一)再探构造函数:初始化列表

1. 基础概念解释

大白话时间:

之前我们写构造函数时,都是在函数体里面给成员变量赋值,就像这样:

cpp 复制代码
Date(int year, int month, int day) {
    _year = year;   // 这是赋值,不是初始化!
    _month = month;
    _day = day;
}

但其实,这叫赋值 ,不叫初始化!真正的初始化是在对象创建的时候就完成的,就像你出生时就有了名字,而不是出生后再取名字。

初始化列表 就是真正做初始化的地方,格式是在构造函数后面加个冒号:,然后用逗号分隔每个成员的初始化:

cpp 复制代码
Date(int year, int month, int day)
    : _year(year)    // 这才是真正的初始化!
    , _month(month)
    , _day(day)
{}

重要规则:

  • 必须用初始化列表的三种情况:引用成员、const 成员、没有默认构造的自定义类型成员

  • ✅ 成员变量的初始化顺序是按声明顺序,不是按初始化列表的顺序!

  • ✅ 即使你不写初始化列表,编译器也会自动生成一个

  • ✅ C++11 支持在声明时给缺省值,这个缺省值就是给初始化列表用的


2. 代码示例

【示例 1】必须使用初始化列表的场景

这个例子展示了哪些情况必须用初始化列表,否则编译报错!

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

// Time类没有默认构造函数
class Time {
public:
    Time(int hour)
        : _hour(hour) 
    {
        cout << "Time(int hour) 被调用" << endl;
    }
private:
    int _hour;
};

class Date {
public:
    // 引用、const成员、无默认构造的自定义类型,必须在初始化列表初始化!
    Date(int& x, int year = 1, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
        , _t(12)          // Time没有默认构造,必须在这里初始化
        , _ref(x)         // 引用必须初始化
        , _n(100)         // const成员必须初始化
    {
        // 如果不在上面初始化,这里就会报错!
        // error: 没有合适的默认构造函数
        // error: 必须初始化引用
        // error: 必须初始化常量限定类型的对象
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
        cout << "引用值: " << _ref << ", const值: " << _n << endl;
    }

private:
    int _year;
    int _month;
    int _day;
    Time _t;        // 没有默认构造的类类型
    int& _ref;      // 引用成员
    const int _n;   // const成员
};

int main() {
    int i = 666;//i的值是传给了int& x中的x
    Date d1(i);
    d1.Print();
    return 0;
}

运行结果:

cpp 复制代码
Time(int hour) 被调用
1-1-1
引用值: 666, const值: 100

【示例 2】初始化顺序的坑!

这个例子告诉你:初始化顺序是按声明顺序,不是按初始化列表顺序!

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

class A {
public:
    A(int a)
        : _a1(a)       // 第二步:初始化_a1,值为1
        , _a2(_a1)     // 第一步:初始化_a2,此时_a1还是随机值!
    {}

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

private:
    // 注意:声明顺序是 _a2 先,_a1 后!
    int _a2 = 2;   // 先初始化_a2
    int _a1 = 2;   // 后初始化_a1
};

int main() {
    A aa(1);
    aa.Print();  // 猜猜输出什么?答案:_a1 = 1, _a2 = 2(因为_a2用了缺省值)
    return 0;
}

划重点: 一定要让初始化列表的顺序和成员声明顺序保持一致,避免踩坑!


(二)类型转换

1. 基础概念解释

大白话时间:

C++ 很 "聪明",有时候会自动帮你做类型转换。比如你写A aa = 1;,编译器会自动把1转换成 A 类型的对象,这就叫隐式类型转换

原理是:编译器先用1构造一个临时的 A 对象,然后用这个临时对象拷贝构造aa。现代编译器会优化成直接构造。

但有时候这种自动转换会带来问题,所以 C++ 提供了explicit关键字,加上 explicit 就禁止隐式转换


2. 代码示例

【示例 1】隐式类型转换演示

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

class A {
public:
    // 不加explicit,支持隐式类型转换
    A(int a1)
        : _a1(a1)
    {
        cout << "A(int a1) 构造" << endl;
    }

    // C++11支持多参数隐式转换
    A(int a1, int a2)
        : _a1(a1)
        , _a2(a2)
    {}

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

    int Get() const {
        return _a1 + _a2;
    }

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B {
public:
    B(const A& a)
        : _b(a.Get())
    {
        cout << "B(const A& a) 构造" << endl;
    }

private:
    int _b = 0;
};

int main() {
    cout << "=== 单参数隐式转换 ===" << endl;
    A aa1 = 1;  // 1 隐式转换成A对象
    aa1.Print();

    cout << "\n=== 引用绑定临时对象 ===" << endl;
    const A& aa2 = 1;  // 临时对象具有常性,要用const引用

    cout << "\n=== C++11多参数隐式转换 ===" << endl;
    A aa3 = {2, 3};
    aa3.Print();

    cout << "\n=== 类类型之间的转换 ===" << endl;
    B b = aa3;  // A对象隐式转换成B对象

    return 0;
}

【示例 2】explicit 禁止隐式转换

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

class A {
public:
    // 加上explicit,禁止隐式类型转换!
    explicit A(int a1)
        : _a1(a1)
    {}

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

private:
    int _a1;
};

int main() {
    // A aa1 = 1;  // 编译报错!不允许隐式转换

    A aa1(1);  // ✅ 显式构造可以
    aa1.Print();

    A aa2 = A(2);  // ✅ 显式构造后拷贝也可以
    aa2.Print();

    return 0;
}

建议: 构造函数尽量加上explicit,避免意外的隐式转换带来 bug!


(三)static 成员

1. 基础概念解释

大白话时间:

普通成员变量是每个对象自己一份,就像每个人都有自己的钱包。但static 静态成员是整个类共享一份,就像班级的公共基金,所有同学共用这一份钱。

关键特性:

  • 静态成员变量不属于某个对象,存在静态区,所有对象共享

  • 静态成员变量必须在类外初始化(类里只是声明)

  • 静态成员函数没有 this 指针,所以不能访问非静态成员

  • 静态成员可以通过类名::成员对象.成员访问

  • 静态成员也受访问限定符(public/private)限制


2. 代码示例

【示例 1】统计创建了多少个对象

这个经典例子完美展示了 static 的用法!

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

class A {
public:
    // 构造函数
    A() {
        ++_scount;  // 每创建一个对象,计数+1
        cout << "构造函数,当前对象数:" << _scount << endl;
    }

    // 拷贝构造函数
    A(const A& t) {
        ++_scount;  // 拷贝构造也要计数!
        cout << "拷贝构造,当前对象数:" << _scount << endl;
    }

    // 析构函数
    ~A() {
        --_scount;  // 对象销毁,计数-1
        cout << "析构函数,当前对象数:" << _scount << endl;
    }

    // 静态成员函数:获取当前对象数
    static int GetACount() {
        return _scount;
    }

private:
    // 类里面只是声明!
    static int _scount;
};

// ✅ 静态成员变量必须在类外初始化!
int A::_scount = 0;

int main() {
    cout << "初始对象数:" << A::GetACount() << endl;  // 通过类名访问

    A a1, a2;
    A a3(a1);  // 拷贝构造

    cout << "\n创建3个对象后:" << endl;
    cout << "通过类名访问:" << A::GetACount() << endl;
    cout << "通过对象访问:" << a1.GetACount() << endl;

    // cout << A::_scount << endl;  // 编译报错!_scount是private的

    return 0;
}

运行结果:

cpp 复制代码
初始对象数:0
构造函数,当前对象数:1
构造函数,当前对象数:2
拷贝构造,当前对象数:3

创建3个对象后:
通过类名访问:3
通过对象访问:3
析构函数,当前对象数:2
析构函数,当前对象数:1
析构函数,当前对象数:0

【示例 2】静态成员函数没有 this 指针

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

class Test {
public:
    static void Func() {
        // cout << _a << endl;  // ❌ 报错!静态函数没有this指针,不能访问非静态成员
        cout << "_s = " << _s << endl;  // ✅ 可以访问静态成员
    }

    void NormalFunc() {
        cout << "_a = " << _a << endl;    // ✅ 非静态函数可以访问普通成员
        cout << "_s = " << _s << endl;    // ✅ 非静态函数也可以访问静态成员
    }

private:
    int _a = 10;        // 普通成员变量
    static int _s;      // 静态成员变量
};

int Test::_s = 20;

int main() {
    Test::Func();  // 不需要对象,直接通过类名调用静态函数

    Test t;
    t.NormalFunc();

    return 0;
}

(四)友元

1. 基础概念解释

大白话时间:

类的封装就像你家的房子,private 成员就是你卧室里的东西,外人不能随便进。但你最好的朋友来了,你肯定会给他开门,让他随便参观。

友元就是 C++ 给你的 "开门权限"!友元可以突破访问限定符,直接访问类的私有成员。

友元分两种:

  • 友元函数:一个函数成为某个类的朋友

  • 友元类:整个类都成为朋友

注意事项:

  • 友元是单向的:A 是 B 的朋友 ≠ B 是 A 的朋友

  • 友元不能传递:A 是 B 的朋友,B 是 C 的朋友 ≠ A 是 C 的朋友

  • 友元会破坏封装,不要滥用!


2. 代码示例

【示例 1】友元函数

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

// 前置声明,告诉编译器B类存在
class B;

class A {
    // 声明func函数是A的友元
    friend void func(const A& aa, const B& bb);

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B {
    // 声明func函数也是B的友元
    friend void func(const A& aa, const B& bb);

private:
    int _b1 = 3;
    int _b2 = 4;
};

// 这个函数同时是A和B的友元,可以访问两个类的私有成员!
void func(const A& aa, const B& bb) {
    cout << "访问A的私有成员:" << aa._a1 << ", " << aa._a2 << endl;
    cout << "访问B的私有成员:" << bb._b1 << ", " << bb._b2 << endl;
}

int main() {
    A aa;
    B bb;
    func(aa, bb);
    return 0;
}

运行结果:

cpp 复制代码
访问A的私有成员:1, 2
访问B的私有成员:3, 4

【示例 2】友元类

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

class A {
    // 声明B类是A的友元,B的所有成员函数都能访问A的私有成员
    friend class B;

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B {
public:
    void func1(const A& aa) {
        cout << "func1访问A的私有:" << aa._a1 << endl;
        cout << "B自己的成员:" << _b1 << endl;
    }

    void func2(const A& aa) {
        cout << "func2访问A的私有:" << aa._a2 << endl;
        cout << "B自己的成员:" << _b2 << endl;
    }

private:
    int _b1 = 3;
    int _b2 = 4;
};

int main() {
    A aa;
    B bb;
    bb.func1(aa);
    bb.func2(aa);

    // 注意:友元是单向的!A不能访问B的私有成员
    return 0;
}

(五)内部类

1. 基础概念解释

大白话时间:

如果一个类定义在另一个类的里面,里面那个就叫内部类。就像你家房子里还有个小房间。

关键特性:

  • 内部类是独立的类,外部类的对象大小不包含内部类

  • 内部类默认就是外部类的友元,可以直接访问外部类的私有成员

  • 内部类受外部类的类域和访问限定符限制

  • 如果把内部类放在 private 里,那它就是外部类的 "专属工具人",外面用不了


2. 代码示例

【示例 1】内部类基本用法

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

class A {
private:
    static int _k;    // 静态成员
    int _h = 1;       // 普通成员

public:
    // B是A的内部类
    class B {
    public:
        void foo(const A& a) {
            cout << "访问A的静态成员:" << _k << endl;    // ✅ 直接访问
            cout << "访问A的普通成员:" << a._h << endl;  // ✅ 内部类默认是友元!
        }

    private:
        int _b1 = 10;
    };
};

// 静态成员初始化
int A::_k = 666;

int main() {
    cout << "A类的大小:" << sizeof(A) << endl;  // 输出4,不包含B!

    // 内部类的定义方式:外部类::内部类
    A::B b;

    A aa;
    b.foo(aa);

    return 0;
}

运行结果:

cpp 复制代码
A类的大小:4
访问A的静态成员:666
访问A的普通成员:1

(六)匿名对象

1. 基础概念解释

大白话时间:

我们平时定义对象都是A aa(1);,这叫有名对象,生命周期是整个作用域。

但有时候我们只需要用一下这个对象,用完就扔,那可以不用给它起名字!这就是匿名对象A(1);

特点:

  • 匿名对象的生命周期只有当前这一行,执行完就析构

  • 格式:类名(参数)

  • 适合临时用一下的场景,非常方便


2. 代码示例

【示例 1】匿名对象的生命周期

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

class A {
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A(int a) 构造,a = " << _a << endl;
    }

    ~A() {
        cout << "~A() 析构,a = " << _a << endl;
    }

private:
    int _a;
};

class Solution {
public:
    int Sum_Solution(int n) {
        return n * (n + 1) / 2;
    }
};

int main() {
    cout << "=== 有名对象 ===" << endl;
    A aa1(1);  // 有名对象,生命周期到main结束

    cout << "\n=== 匿名对象 ===" << endl;
    A();       // 匿名对象,这一行结束就析构
    A(2);      // 另一个匿名对象

    cout << "\n=== 匿名对象的妙用 ===" << endl;
    // 只用一次的函数,不用创建对象!直接用匿名对象调用
    cout << "1+2+...+10 = " << Solution().Sum_Solution(10) << endl;

    cout << "\n=== main函数结束 ===" << endl;
    return 0;
}

运行结果(注意析构顺序):

cpp 复制代码
=== 有名对象 ===
A(int a) 构造,a = 1

=== 匿名对象 ===
A(int a) 构造,a = 0
~A() 析构,a = 0
A(int a) 构造,a = 2
~A() 析构,a = 2

=== 匿名对象的妙用 ===
1+2+...+10 = 55

=== main函数结束 ===
~A() 析构,a = 1

看到了吗? 匿名对象用完马上就析构了,而有名对象要等到作用域结束!


(七)对象拷贝时的编译器优化

1. 基础概念解释

大白话时间:

现代编译器都很 "聪明",为了提高效率,会在不影响结果的前提下,帮我们省略一些不必要的拷贝构造。

比如你写A aa = 1;,理论上应该是:

  1. 用 1 构造一个临时 A 对象

  2. 用临时对象拷贝构造 aa

但编译器会优化成:直接用 1 构造 aa,省掉了拷贝这一步!

常见优化场景:

  • 传值传参时,连续的构造 + 拷贝构造 → 优化为直接构造

  • 传值返回时,连续的拷贝构造 + 拷贝构造 → 优化为一次拷贝

  • 不同编译器优化程度不同,VS2022 比 VS2019 优化更激进


2. 代码示例

【示例 1】编译器优化完整演示

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

class A {
public:
    A(int a = 0)
        : _a1(a)
    {
        cout << "A(int a) 构造" << endl;
    }

    A(const A& aa)
        : _a1(aa._a1)
    {
        cout << "A(const A& aa) 拷贝构造" << endl;
    }

    A& operator=(const A& aa) {
        cout << "A& operator= 赋值重载" << endl;
        if (this != &aa) {
            _a1 = aa._a1;
        }
        return *this;
    }

    ~A() {
        cout << "~A() 析构" << endl;
    }

private:
    int _a1 = 1;
};

// 传值传参
void f1(A aa) {}

// 传值返回
A f2() {
    A aa;
    return aa;
}

int main() {
    cout << "===== 场景1:传值传参 =====" << endl;
    A aa1;
    f1(aa1);  // 拷贝构造,无法优化
    cout << endl;

    cout << "===== 场景2:隐式类型转换优化 =====" << endl;
    f1(1);    // 构造+拷贝构造 → 优化为直接构造!
    cout << endl;

    cout << "===== 场景3:匿名对象传参优化 =====" << endl;
    f1(A(2)); // 构造+拷贝构造 → 优化为直接构造!
    cout << endl;

    cout << "===== 场景4:传值返回 =====" << endl;
    f2();     // 不同编译器优化程度不同
    cout << endl;

    cout << "===== 场景5:返回值接收优化 =====" << endl;
    A aa2 = f2();  // 连续拷贝构造 → 优化!
    cout << endl;

    cout << "===== 场景6:赋值无法优化 =====" << endl;
    aa1 = f2();    // 赋值重载,无法优化
    cout << endl;

    return 0;
}

VS2022 下的运行结果(优化后):

cpp 复制代码
===== 场景1:传值传参 =====
A(int a) 构造
A(const A& aa) 拷贝构造
~A() 析构

===== 场景2:隐式类型转换优化 =====
A(int a) 构造        // 直接构造,没有拷贝!
~A() 析构

===== 场景3:匿名对象传参优化 =====
A(int a) 构造        // 直接构造,没有拷贝!
~A() 析构

===== 场景4:传值返回 =====
A(int a) 构造
~A() 析构

===== 场景5:返回值接收优化 =====
A(int a) 构造        // VS2022直接优化成一次构造!
~A() 析构

===== 场景6:赋值无法优化 =====
A(int a) 构造
A(const A& aa) 拷贝构造
A& operator= 赋值重载
~A() 析构
~A() 析构

~A() 析构
~A() 析构

划重点: 编译器优化是 "锦上添花",我们写代码时还是要按正常逻辑写,不要依赖优化!


总结

今天我们学习了类和对象的 7 个重要知识点:

|---------------|-------------------------------------|
| 知识点 | 核心要点 |
| 初始化列表 | 真正的初始化,引用 /const/ 无默认构造必须用,按声明顺序初始化 |
| 类型转换 | 隐式转换很方便但有风险,用 explicit 禁止不需要的转换 |
| static 成员 | 全类共享,类外初始化,静态函数无 this 指针 |
| 友元 | 突破封装的后门,单向、不传递,慎用 |
| 内部类 | 定义在类里面,默认是友元,受类域限制 |
| 匿名对象 | 生命周期只有一行,用完就扔,临时用很方便 |
| 编译器优化 | 连续的构造 + 拷贝会优化,我们按正常逻辑写就行 |

这些都是 C++ 面向对象的核心知识,一定要好好理解!下一篇我们会学习,敬请期待~


学习 C++ 一定要多动手写代码,光看是学不会的!把今天的例子都自己敲一遍运行看看,你会理解得更深刻。有问题欢迎在评论区留言哦


【从零开始学 C++】系列文章:

  • 第一篇:C++ 入门基础

  • 第二篇:类和对象(上)

  • 第三篇:类和对象(中)

  • 第四篇:类和对象(下)← 你在这里

  • 第五篇:努力创作中,尽请期待!

相关推荐
覆东流1 小时前
第10天:python元组
开发语言·后端·python
CSCN新手听安1 小时前
【Qt】系统相关(一)内容简介,事件概念,事件的处理
开发语言·c++·qt
不想写代码的星星1 小时前
重识 std::tuple:一个被低估的编译期异构容器
开发语言·c++
aqiu1111112 小时前
[特殊字符]【算法日记 14】数论入门神题:最大公约数与最小公倍数的“乘积守恒定律”
算法
techdashen2 小时前
用 Rust 写生产级服务要踩多少坑——Cloudflare 把答案做成了一个开源库
开发语言·rust·开源
码界奇点2 小时前
基于Python的微信公众号爬虫系统设计与实现
开发语言·爬虫·python·毕业设计·web·源代码管理
保卫大狮兄2 小时前
一文讲清:仓库管理最核心的10个公式
人工智能·算法·仓库管理
瞎折腾啥啊2 小时前
VCPKG详细使用教程
linux·c++·cmake·cmakelists
落雪寒窗-2 小时前
Python开发个人日常记录
开发语言·python