C++ 类和对象(下):进阶特性与实战技巧

        在 C++ 类和对象的学习中,除了基础的默认成员函数,还有诸多进阶特性支撑着灵活且高效的代码编写。这些特性包括构造函数的初始化列表、类型转换控制、static 成员、友元、内部类、匿名对象以及编译器的拷贝优化等。它们既是面试高频考点,也是实际开发中提升代码质量的关键。本文将结合实例,逐一拆解这些特性的核心原理与使用场景,帮你构建完整的 C++ 面向对象知识体系。

一、再探构造函数:初始化列表的核心逻辑

之前实现构造函数时,我们习惯在函数体内给成员变量赋值,但这种方式本质是 “赋值” 而非 “初始化”。C++ 提供了初始化列表,作为成员变量的 “真正初始化场所”,尤其对特殊成员(引用、const、无默认构造的自定义类型)至关重要。

1.1 初始化列表的基础语法

初始化列表以冒号开头,后跟逗号分隔的成员初始化项,每个项格式为成员变量(初始值/表达式)。例如:

class Date {
public:
    // 初始化列表:_year、_month、_day在列表中初始化
    Date(int year, int month, int day) 
        : _year(year)
        , _month(month)
        , _day(day)
    {} // 函数体可空(若无需额外逻辑)

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

1.2 必须用初始化列表的三种场景

以下三类成员变量无法通过函数体赋值初始化,必须在初始化列表中指定初始值,否则编译报错:

  1. 引用成员变量:引用必须在定义时绑定对象,无法后续赋值。
  2. const 成员变量:const 变量必须在定义时初始化,后续不可修改。
  3. 无默认构造的自定义类型成员:若自定义类型没有无参构造或全缺省构造(即编译器无法自动生成默认构造),必须显式传参初始化。

示例:三种特殊成员的初始化

class Time {
public:
    // Time无默认构造(必须传hour)
    Time(int hour) : _hour(hour) {}
private:
    int _hour;
};

class Date {
public:
    // 必须用初始化列表初始化以下成员
    Date(int& ref, int hour)
        : _ref(ref)       // 引用成员:绑定外部变量ref
        , _n(10)          // const成员:初始化值10
        , _t(hour)        // 无默认构造的Time:传参hour
        , _year(2024)     // 普通成员:也建议在列表初始化
    {}

private:
    int& _ref;       // 引用
    const int _n;    // const
    Time _t;         // 无默认构造的自定义类型
    int _year;       // 普通成员
};

1.3 初始化列表的关键规则

  1. 成员初始化顺序 = 类中声明顺序:与初始化列表中的先后顺序无关。例如,若类中声明顺序是_a2_a1,则列表中_a1(1), _a2(_a1)会先初始化_a2(用未初始化的_a1,导致随机值)。

    class A {
    public:
        A(int a) : _a1(a), _a2(_a1) {} // 危险:_a2先初始化,_a1未定义
        void Print() { cout << _a1 << " " << _a2 << endl; }
    private:
        int _a2; // 声明顺序1:先初始化
        int _a1; // 声明顺序2:后初始化
    };
    int main() {
        A aa(1); 
        aa.Print(); // 输出:1 随机值(因_a2用未初始化的_a1赋值)
    }
    

    建议:初始化列表顺序与类中声明顺序保持一致,避免逻辑错误。

  2. 缺省值与初始化列表的配合:C++11 支持在成员声明时给缺省值(如int _year = 2024),该缺省值仅在成员未出现在初始化列表时生效。例如:

    class Date {
    public:
        Date() : _month(2) {} // _month在列表初始化(值2)
        // _year用声明时的缺省值1,_day未给缺省值(编译器可能初始化为随机值)
    private:
        int _year = 1;  // 缺省值1
        int _month = 1; // 缺省值1(被列表覆盖)
        int _day;       // 无缺省值
    };
    
  3. 所有成员都会走初始化列表:即使未显式写在列表中,成员也会通过初始化列表初始化(普通内置类型用缺省值或随机值,自定义类型调用默认构造)。

二、类型转换:控制类对象的隐式转换

C++ 允许内置类型与类类型之间的隐式转换,也支持类类型之间的隐式转换,但过度隐式转换可能导致逻辑歧义。通过explicit关键字可限制这种转换,提升代码安全性。

2.1 内置类型转类类型:单参数构造函数的作用

若类有单个参数的构造函数(或除第一个参数外其余参数都有缺省值的多参数构造函数),编译器会自动用该构造函数实现内置类型到类类型的隐式转换。

示例:内置类型隐式转类类型

class A {
public:
    // 单参数构造函数:支持int转A
    A(int a1) : _a1(a1) {}
    void Print() { cout << _a1 << endl; }
private:
    int _a1;
};

int main() {
    A aa1 = 1; // 隐式转换:用1构造A临时对象,再拷贝构造aa1(编译器优化为直接构造)
    aa1.Print(); // 输出:1

    const A& aa2 = 2; // 隐式转换:临时对象绑定到const引用(延长生命周期)
    aa2.Print(); // 输出:2

    // C++11支持多参数隐式转换(需用花括号)
    class B {
    public:
        B(int x, int y = 0) : _x(x), _y(y) {}
        void Print() { cout << _x << " " << _y << endl; }
    private:
        int _x, _y;
    };
    B bb = {3, 4}; // 多参数隐式转换:用{3,4}构造B
    bb.Print(); // 输出:3 4
}

2.2 禁止隐式转换:explicit 关键字

若不希望允许内置类型隐式转类类型,可在构造函数前加explicit关键字,此时只能显式构造对象。

示例:explicit 限制隐式转换

class A {
public:
    // explicit修饰:禁止隐式转换
    explicit A(int a1) : _a1(a1) {}
private:
    int _a1;
};

int main() {
    A aa1(1); // 显式构造:合法
    // A aa2 = 2; // 编译报错:explicit禁止隐式转换
    // const A& aa3 = 3; // 编译报错:同上
}

2.3 类类型之间的隐式转换

类类型 A 转类类型 B,需在 B 中定义以 A 为参数的构造函数(或在 A 中定义转换函数,后续章节讲解)。

示例:A 类对象隐式转 B 类对象

class A {
public:
    A(int a) : _a(a) {}
    int GetA() const { return _a; }
private:
    int _a;
};

class B {
public:
    // 以A为参数的构造函数:支持A转B
    B(const A& a) : _b(a.GetA() * 2) {}
    void Print() { cout << _b << endl; }
private:
    int _b;
};

int main() {
    A aa(5);
    B bb = aa; // 隐式转换:用aa构造B对象
    bb.Print(); // 输出:10(5*2)
}

三、static 成员:类共享的变量与函数

在成员变量或成员函数前加static,即成为 static 成员。static 成员属于整个类,而非某个对象,所有对象共享同一 static 成员,存放在静态区(不占对象内存)。

3.1 static 成员变量的核心特性

  1. 类内声明,类外初始化:static 成员变量不能在类内初始化(即使给缺省值也不行),必须在类外单独初始化(格式:类型 类名::变量名 = 初始值)。
  2. 不依赖对象访问:可通过类名::变量名直接访问(无需创建对象),也可通过对象.变量名访问(不推荐,易误解为对象独有)。
  3. 受访问限定符限制:若 static 成员在private下,外部无法直接访问,需通过 public 的成员函数间接访问。

示例:用 static 成员统计对象个数

#include<iostream>
using namespace std;

class A {
public:
    // 构造:创建对象时计数+1
    A() { ++_scount; }
    // 拷贝构造:拷贝对象时计数+1
    A(const A&) { ++_scount; }
    // 析构:销毁对象时计数-1
    ~A() { --_scount; }

    // public成员函数:获取对象个数(static函数,无需对象调用)
    static int GetCount() { return _scount; }

private:
    // static成员变量:类内声明
    static int _scount;
};

// static成员变量:类外初始化(初始值0)
int A::_scount = 0;

int main() {
    // 无对象时,计数为0
    cout << "初始对象数:" << A::GetCount() << endl; // 输出:0

    A a1, a2;          // 创建2个对象,计数=2
    A a3(a1);          // 拷贝1个对象,计数=3
    cout << "当前对象数:" << A::GetCount() << endl; // 输出:3

    // cout << A::_scount << endl; // 编译报错:_scount是private
    return 0;
}

3.2 static 成员函数的核心特性

  1. 无 this 指针:static 成员函数不依赖对象,因此没有隐含的this指针,无法访问非 static 成员(非 static 成员需通过 this 指针定位对象)。
  2. 访问权限:只能访问 static 成员变量和 static 成员函数,可被非 static 成员函数调用(非 static 有 this 指针,可间接访问 static)。
  3. 调用方式:与 static 成员变量一致,通过类名::函数名对象.函数名调用。

3.3 static 成员的经典应用

        统计对象个数:如上述示例,跟踪类的实例化次数。

        实现单例模式:通过 static 成员变量保存唯一实例,禁止外部创建对象。

        共享资源:如配置信息、全局计数器等,多个对象共享同一数据。

四、友元:突破封装的 “特殊通道”

C++ 的封装特性要求类的私有成员只能被自身成员函数访问,但友元提供了一种 “例外机制”—— 允许外部函数或类访问当前类的私有成员。友元分为友元函数友元类,但需注意:友元会破坏封装,增加耦合度,应谨慎使用。

4.1 友元函数

友元函数是在类内声明时加friend关键字的外部函数,它不是类的成员函数,但可直接访问类的私有成员。

示例:友元函数访问两个类的私有成员

#include<iostream>
using namespace std;

// 前置声明:告诉编译器B是一个类(否则A的友元函数声明无法识别B)
class B;

class A {
public:
    A() : _a(1) {}
    // 友元声明:func是A的友元函数,可访问A的私有成员
    friend void func(const A& aa, const B& bb);
private:
    int _a;
};

class B {
public:
    B() : _b(2) {}
    // 友元声明:func是B的友元函数,可访问B的私有成员
    friend void func(const A& aa, const B& bb);
private:
    int _b;
};

// 友元函数:外部定义,可访问A和B的私有成员
void func(const A& aa, const B& bb) {
    cout << "A的私有成员:" << aa._a << endl; // 输出:1
    cout << "B的私有成员:" << bb._b << endl; // 输出:2
}

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

友元函数的规则

        可在类的任意位置声明(public/private/protected 均可,不受访问限定符限制)。

        一个函数可同时是多个类的友元(如上述func是 A 和 B 的友元)。

4.2 友元类

若类 C 是类 D 的友元类,则 C 的所有成员函数都可访问 D 的私有成员。友元类的关系是单向、不可传递的:

        单向:若 C 是 D 的友元,D 不一定是 C 的友元。

        不可传递:若 C 是 D 的友元,D 是 E 的友元,C 不一定是 E 的友元。

示例:友元类的使用

class A {
public:
    A() : _a1(1), _a2(2) {}
    // 友元声明:B是A的友元类,B的所有成员函数可访问A的私有成员
    friend class B;
private:
    int _a1;
    int _a2;
};

class B {
public:
    // B的成员函数:访问A的私有成员
    void PrintA(const A& aa) {
        cout << "A::_a1 = " << aa._a1 << endl; // 输出:1
        cout << "A::_a2 = " << aa._a2 << endl; // 输出:2
    }
private:
    int _b = 3;
};

int main() {
    A aa;
    B bb;
    bb.PrintA(aa); // B的成员函数访问A的私有成员,合法
    return 0;
}

五、内部类:类中的 “嵌套类”

若一个类定义在另一个类的内部,该类称为内部类(嵌套类)。内部类是独立的类,仅受外部类的类域和访问限定符限制,外部类的对象不包含内部类的成员(即内部类不占外部类的内存)。

5.1 内部类的核心特性

  1. 默认是外部类的友元:内部类可直接访问外部类的 static 成员和私有成员(无需显式声明友元)。
  2. 访问方式:需通过外部类名::内部类名访问内部类(如A::B b),若内部类在外部类的 private 下,外部无法访问。
  3. 内存独立:外部类的大小与内部类无关(仅包含自身成员)。

示例:内部类的使用

#include<iostream>
using namespace std;

class A {
private:
    static int _k; // 外部类的static成员
    int _h = 1;    // 外部类的私有成员
public:
    // 内部类B:定义在A的public下,外部可访问
    class B {
    public:
        void PrintA(const A& a) {
            // 内部类默认是A的友元,可访问A的static和私有成员
            cout << "A::_k = " << A::_k << endl; // 访问static成员
            cout << "A::_h = " << a._h << endl;  // 访问私有成员(需A对象)
        }
    };
};

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

int main() {
    // 计算A的大小:仅包含_h(int,4字节),与B无关
    cout << "sizeof(A) = " << sizeof(A) << endl; // 输出:4

    // 创建内部类对象:需用A::B
    A::B b;
    A a;
    b.PrintA(a); // 输出:A::_k = 10;A::_h = 1
    return 0;
}

5.2 内部类的应用场景

当两个类强关联(如 A 类的功能完全为 B 类服务),且不希望 A 类被外部其他类访问时,可将 A 类设计为 B 类的内部类,并放在 private 下,实现 “专属封装”。例如:实现队列时,可将节点类作为队列类的内部私有类。

六、匿名对象:临时使用的 “无名称对象”

通常我们创建对象时会指定名称(如A aa),称为有名对象。C++ 还支持匿名对象,格式为类型(实参),无需指定名称,生命周期仅在当前行(行结束后自动析构)。

6.1 匿名对象的基础使用

匿名对象适合 “临时使用一次” 的场景,避免创建不必要的有名对象。例如:

class A {
public:
    A(int a = 0) : _a(a) {
        cout << "A(int a):" << _a << endl;
    }
    ~A() {
        cout << "~A():" << _a << endl;
    }
    void Print() {
        cout << "A::_a = " << _a << endl;
    }
private:
    int _a;
};

int main() {
    // 有名对象:生命周期到main函数结束
    A aa1(1); 
    aa1.Print();

    // 匿名对象:生命周期仅当前行,行结束后析构
    A(2).Print(); // 输出:A::_a = 2;随后调用~A()

    // 匿名对象作为函数参数(临时传参,无需创建有名对象)
    void Func(A a) {}
    Func(A(3)); // 传递匿名对象,使用后析构

    return 0;
}

输出顺序

A(int a):1
A::_a = 1
A(int a):2
A::_a = 2
~A():2
A(int a):3
~A():3
~A():1

6.2 匿名对象的经典场景

        临时调用成员函数:如Solution().Sum_Solution(10)(调用 Solution 类的 Sum_Solution 函数,无需创建有名对象)。

        简化代码:避免为临时使用的对象取名,减少代码冗余。

七、对象拷贝的编译器优化

现代编译器会在不影响代码正确性的前提下,对对象的拷贝操作(拷贝构造、赋值重载)进行优化,减少不必要的临时对象创建,提升效率。优化规则主要是 “合并连续的拷贝构造为直接构造”。

7.1 常见的优化场景

A类为例(含构造、拷贝构造、析构),分析几种典型场景的优化:

场景 1:隐式转换 + 拷贝构造优化

void Func(A a) {}

int main() {
    // 未优化:int→A临时对象(构造)→拷贝构造到Func参数a
    // 优化后:直接用1构造Func参数a(仅1次构造)
    Func(1); 
}
场景 2:匿名对象 + 拷贝构造优化

void Func(A a) {}

int main() {
    // 未优化:A(2)构造→拷贝构造到Func参数a
    // 优化后:直接用2构造Func参数a(仅1次构造)
    Func(A(2)); 
}
场景 3:函数返回值优化(RVO/NRVO)

A Func() {
    A a(3);
    return a; // 未优化:a拷贝构造临时对象→返回
}

int main() {
    // 未优化:Func返回的临时对象→拷贝构造aa
    // 优化后:直接构造aa(仅1次构造,跳过临时对象)
    A aa = Func(); 
}

7.2 关闭优化以观察原始行为

若需观察未优化的拷贝过程,可在编译时添加参数关闭优化:

        GCC/G++:g++ test.cpp -fno-elide-constructors(关闭拷贝优化)。

        VS:在项目属性中关闭 “代码优化”(Debug 模式默认关闭优化,Release 模式默认开启)。

八、总结:进阶特性的核心应用指南

C++ 类的进阶特性看似零散,实则各有明确的设计目标。以下是关键特性的应用场景总结,帮你在实际开发中灵活选用:

特性核心作用注意事项
初始化列表初始化特殊成员(引用、const、无默认构造类型)初始化顺序与类声明一致,避免逻辑错误
explicit禁止内置类型隐式转类类型需显式构造对象,提升代码安全性
static 成员实现类共享资源(计数器、配置)类外初始化,无 this 指针,不能访问非 static 成员
友元突破封装,允许外部访问私有成员破坏封装,增加耦合度,谨慎使用
内部类实现强关联类的专属封装独立类,不占外部类内存,默认是友元
匿名对象临时使用一次的对象生命周期仅当前行,避免不必要的有名对象
编译器拷贝优化减少临时对象,提升效率不依赖优化写代码,优化是编译器的 “额外福利”

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值