【C++ 入门】类和对象下

大家好!之前我们聊了类和对象的基础,比如构造函数、析构函数这些 "标配"。今天咱们再往深走一步,看看 C++ 里几个超实用的进阶特性 ------初始化列表静态成员友元内部类。这些知识点能帮你写出更灵活、更高效的代码,但也容易踩坑,咱们一个个掰扯清楚,每个点都附上手写代码和配图建议,保证新手也能看懂~

一、构造函数的 "优化方案":初始化列表

之前咱们写构造函数时,习惯在函数体里给成员变量赋值,比如这样:

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. 初始化列表怎么写?

格式很简单:构造函数声明后加冒号,后面跟 "成员变量 (初始值)" 的列表,用逗号分隔。咱们把上面的 Date 类改成初始化列表版本:

cpp

复制代码
class Date {
public:
    // 初始化列表:冒号开始,成员变量(初始值)用逗号分隔
    Date(int& xx, int year, int month, int day)
        : _year(year)    // 用形参year初始化成员_year
        , _month(month)  // 用形参month初始化成员_month
        , _day(day)      // 用形参day初始化成员_day
        , _n(1)          // const成员必须在这里初始化
        , _ref(xx)       // 引用成员必须在这里初始化
        , _t(1)          // 自定义类型Time没有默认构造,必须这里初始化
        , _ptr((int*)malloc(12))  // 指针成员初始化(分配12字节空间)
    {
        // 函数体里可以做额外操作(比如检查指针是否为空)
        if (_ptr == nullptr) {
            perror("malloc fail");  // 打印内存分配失败信息
        } else {
            memset(_ptr, 0, 12);    // 把分配的空间清0
        }
    }

    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    // 成员变量声明(注意顺序!初始化顺序和声明顺序一致)
    int _year;
    int _month;
    int _day;
    const int _n;       // const成员:只能初始化,不能赋值
    int& _ref;          // 引用成员:必须绑定一个变量,不能单独存在
    class Time {        // 自定义类型Time:没有默认构造函数(必须传参)
    public:
        Time(int hour) : _hour(hour) {}  // 只有带参构造
    private:
        int _hour;
    } _t;  // 成员变量_t:类型是Time
    int* _ptr;          // 指针成员
};

2. 初始化列表的 3 个 "铁规则"

这些规则记不住,编译时必报错,一定要划重点!

  1. 特殊成员必须用初始化列表以下 3 种成员,只能在初始化列表里初始化,函数体里赋值会报错:

    • 引用成员(如int& _ref):引用必须在定义时绑定变量,没法 "先定义再赋值";
    • const 成员(如const int _n):const 变量一旦初始化就不能改,函数体里赋值是 "修改",不允许;
    • 没有默认构造的自定义类型(如Time _t):如果自定义类型没有 "不用传参的构造函数",必须在初始化列表里传参构造。
  2. 每个成员只能初始化一次 初始化列表里,一个成员变量只能出现一次(比如不能同时写_year(year), _year(2024)),因为 "初始化" 只能做一次。

  3. 初始化顺序 = 成员声明顺序(和列表顺序无关) 这是最容易踩的坑!比如你在列表里先写_month(month), _year(year),但成员声明是_year在前、_month在后,那么实际初始化顺序还是先_year_month。✅ 建议:让 "初始化列表顺序" 和 "成员声明顺序" 保持一致,避免逻辑错误。

3. 小补充:C++11 的成员缺省值

如果某个成员变量大部分情况下都是同一个初始值,可以在声明时直接给缺省值。如果初始化列表没处理这个成员,就会用缺省值:

cpp

复制代码
class Date {
private:
    int _year = 2024;  // 缺省值:没在初始化列表写,就用2024
    int _month = 1;    // 缺省值:没写就用1
    int _day;          // 没缺省值:没在列表写,内置类型是随机值
};

📌 小贴士:尽量用初始化列表!哪怕是普通成员,初始化列表也比函数体赋值更高效~(图 1:初始化列表执行流程示意图 ------ 可手绘 "对象内存分配 → 初始化列表执行 → 构造函数体执行" 的步骤)

二、被所有对象 "共享" 的静态成员

有时候我们需要一个 "全局变量",但又想让它只属于某个类(比如统计一个类创建了多少个对象)。这时候「静态成员」就派上用场了 ------ 用static修饰的成员,所有对象共享,不占单个对象的内存。

1. 静态成员变量:所有对象的 "公共财产"

核心性质:
  • 存放在静态区(不是栈 / 堆),程序启动时分配,结束时释放;
  • 所有对象共享同一个静态成员变量(改一个对象的静态成员,其他对象看到的也会变);
  • 必须在类外初始化 (类内只能声明,不能赋值,除非是const static整型)。
代码示例:统计对象创建个数

cpp

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

class Test {
public:
    Test() {
        _count++;  // 每次创建对象,静态成员_count加1
    }

    // 静态成员函数:用来访问私有静态成员(后面讲)
    static int GetCount() {
        return _count;
    }

private:
    // 静态成员变量:声明(类内只能声明)
    static int _count;
};

// 静态成员变量:类外初始化(必须写!格式:类名::成员名 = 初始值)
int Test::count = 0;  // 这里的_count和类里的是同一个

int main() {
    Test t1, t2, t3;  // 创建3个对象
    // 访问静态成员:类名::成员名(因为是public静态函数)
    cout << "创建的对象个数:" << Test::GetCount() << endl;  // 输出3
    return 0;
}

2. 静态成员函数:没有 this 指针的 "特殊函数"

静态成员函数用static修饰,和普通成员函数最大的区别是:没有 this 指针(因为它不属于某个具体对象)。

核心规则:
  • 只能访问静态成员(变量 / 函数),不能访问非静态成员(因为没有 this 指针,找不到具体对象的成员);
  • 非静态成员函数可以访问静态成员(非静态有 this 指针,但静态成员是共享的,不用 this 找);
  • 访问方式:类名::函数名()对象.函数名()(推荐用类名访问,更清晰)。

3. 访问权限:看访问限定符!

很多人以为 "静态成员只能用类名访问",其实不对 ------ 核心看public/private/protected

  • 如果静态成员是public:类外可以直接用类名::成员名访问(比如上面的Test::GetCount());
  • 如果静态成员是private:类外不能直接访问,必须通过public静态成员函数间接访问(比如上面的_count是 private,只能通过GetCount()访问)。

三、突破封装的 "特殊通道":友元

C++ 的类封装得很严,私有成员只能在类内访问。但有时候需要 "开个后门"(比如运算符重载、IO 流操作),这时候「友元」就来了 ------ 它能让外部函数 / 类直接访问类的私有成员。

1. 友元函数:单个函数的 "后门"

特点:
  • 不是类的成员函数,但能访问类的private/protected成员;
  • 声明时加friend,放在类内任意位置(不受访问限定符限制);
  • 一个函数可以是多个类的友元。
代码示例:让 func 访问 A 和 B 的私有成员

cpp

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

// 前置声明:告诉编译器B是个类(否则A里的友元声明不认识B)
class B;

class A {
    // 友元声明:func是A的友元,可以访问A的私有成员
    friend void func(const A& aa, const B& bb);
private:
    int _a1 = 1;  // 私有成员
    int _a2 = 2;
};

class B {
    // 友元声明:func也是B的友元,可以访问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 << endl;  // 正确:访问A::_a1
    cout << "B的私有成员:" << bb._b1 << endl;  // 正确:访问B::_b1
}

int main() {
    A aa;
    B bb;
    func(aa, bb);  // 输出:1 和 3
    return 0;
}

2. 友元类:整个类的 "后门"

如果一个类 A 是类 B 的友元,那么A 的所有成员函数都能访问 B 的私有成员

核心规则:
  • 单向性:A 是 B 的友元 ≠ B 是 A 的友元(B 不能访问 A 的私有成员);
  • 不可传递:A 是 B 的友元,B 是 C 的友元 ≠ A 是 C 的友元;
  • 声明方式:在 B 类里写friend class A;
代码示例:B 是 A 的友元

cpp

复制代码
class A {
    // 友元类声明:B是A的友元,B的所有成员函数能访问A的私有成员
    friend class B;
private:
    int _a1 = 1;
};

class B {
public:
    // B的成员函数:能直接访问A的私有成员
    void PrintA(const A& aa) {
        cout << "A的私有成员:" << aa._a1 << endl;  // 正确
    }
private:
    int _b1 = 3;
};

int main() {
    A aa;
    B bb;
    bb.PrintA(aa);  // 输出:1
    // aa访问B的私有成员?错误!A不是B的友元
    // cout << aa._b1;  // 编译报错
    return 0;
}

⚠️ 注意:友元会破坏封装!能不用就不用,除非确实需要(比如实现operator<<重载)。(图 3:友元函数访问权限测试截图 ------ 展示 func 运行后输出 "A 的私有成员:1" 和 "B 的私有成员:3")

但是如果我们解封下面那段注释,代码一定会报错,因为A不是B的友元。

四、专属 "小助手":内部类

如果一个类 A 和类 B 关系特别紧密(比如 A 就是为 B 服务的),可以把 A 定义在 B 的内部,这就是「内部类」。

1. 内部类的核心性质

  • 独立性:内部类是一个独立的类,外部类的对象不包含内部类的成员(内存上没关系);
  • 域限制:内部类的名字只在外部类的域里有效(比如Solution::Sum, outside 用不了);
  • 默认友元:内部类默认是外部类的友元(内部类能访问外部类的私有成员);
  • 封装性:如果内部类放在外部类的private里,那么只有外部类能使用这个内部类(专属小助手)。

2. 代码示例:用内部类计算 1 到 n 的和

比如我们要实现一个Solution类,里面有个Sum_Solution函数计算 1+2+...+n。可以把求和的逻辑放在内部类Sum里,让Sum成为Solution的专属工具:

cpp

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

class Solution {
public:
    int Sum_Solution(int n) {
        // 创建n个Sum对象:每个对象构造时会累加1~n
        Sum* p = new Sum[n];
        delete[] p;  // 释放内存(避免内存泄漏)
        // 访问内部类的静态成员:外部类::内部类::成员名
        return Sum::GetRet();
    }

private:
    // 内部类Sum:放在private里,只有Solution能使用
    class Sum {
    public:
        Sum() {
            _ret += _i;  // 构造时累加:_ret = 1+2+...+n
            _i++;
        }

        // 静态成员函数:返回累加结果
        static int GetRet() {
            return _ret;
        }

    private:
        static int _i;   // 静态成员:记录当前要加的数(初始1)
        static int _ret; // 静态成员:记录累加结果(初始0)
    };
};

// 内部类的静态成员:类外初始化(格式:外部类::内部类::成员名 = 初始值)
int Solution::Sum::_i = 1;
int Solution::Sum::_ret = 0;

int main() {
    Solution sol;
    cout << "1到10的和:" << sol.Sum_Solution(10) << endl;  // 输出55
    return 0;
}

(图 4:内部类使用场景示意图 ------ 标注 "Solution 类(外部)" 包含 "Sum 类(内部,private)",箭头指向 "Sum 只为 Solution 服务")

总结:这 4 个特性怎么用?

特性 核心作用 注意事项
初始化列表 高效初始化成员(尤其是特殊成员) 初始化顺序 = 声明顺序;特殊成员必须用
静态成员 所有对象共享数据 / 函数 类外初始化;静态函数无 this 指针
友元 突破封装(开后门) 破坏封装,尽量少用;单向不可传递
内部类 封装紧密关联的类 独立存在;默认是外部类的友元;private 专属

其实这些特性不难,关键是多动手敲代码 ------ 比如试着用静态成员统计对象个数,用内部类实现一个简单的计算器,踩几次坑就记住了~ 如果你在实践中遇到问题,欢迎在评论区留言讨论!

相关推荐
小尧嵌入式2 小时前
CANOpen协议
服务器·网络·c++·windows
枫叶丹42 小时前
【Qt开发】Qt事件(二)-> QKeyEvent 按键事件
c语言·开发语言·数据库·c++·qt·microsoft
冷崖6 小时前
原子锁操作
c++·后端
旖旎夜光10 小时前
C++(17)
c++·学习
Larry_Yanan11 小时前
Qt多进程(三)QLocalSocket
开发语言·c++·qt·ui
superman超哥11 小时前
仓颉语言中元组的使用:深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
Lucas5555555512 小时前
现代C++四十不惑:AI时代系统软件的基石与新征程
开发语言·c++·人工智能
_MyFavorite_13 小时前
cl报错+安装 Microsoft Visual C++ Build Tools
开发语言·c++·microsoft
charlie11451419113 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++