Modern C++(七)类

发布于:2025-07-07 ⋅ 阅读:(23) ⋅ 点赞:(0)

7、类

7.1、类声明

前置声明:声明一个将稍后在此作用域定义的类类型。直到定义出现前,此类名具有不完整类型。当代码仅仅需要用到类的指针或引用时,就可以采用前置声明,无需包含完整的类定义。

前置声明有以下几个作用:

  • 降低编译时的依赖性,加快编译速度
  • 避免出现循环引用
// 前置声明
class MyClass;

void func(MyClass* obj); // 可以使用指针
void func2(const MyClass& obj); // 可以使用引用
MyClass* createObj(); // 返回指针或引用是可行的

使用前置声明也会有一些限制,除了不能访问类成员、不能创建类对象、不能使用类的大小信息外,还不能继承自该类。

7.2、局部类

局部类(Local Class)是指定义在函数内部的类,其作用域仅限于该函数。它们无法被函数外部访问。局部类可以访问函数的静态变量、外部全局变量和枚举。局部类的非静态成员函数不能访问外部函数的非静态局部变量。

int globalVar = 100;

void func() {
    static int staticVar = 200;
    int localVar = 300; // 局部变量(非静态)

    class Local {
    public:
        void print() {
            std::cout << globalVar << std::endl; // 合法:访问全局变量
            std::cout << staticVar << std::endl; // 合法:访问静态变量
            // std::cout << localVar << std::endl; // 错误:无法访问非静态局部变量
        }
    };

    Local().print();
}

局部类一般用于封装临时性算法,实现函数内部的策略模式。

7.3、联合体声明

联合体(Union)是C++中一种特殊的类类型,它允许在相同的内存位置存储不同类型的数据。与结构体(struct)和类(class)相比,联合体的主要特点是所有成员共享同一块内存,因此在同一时刻只能存储一个成员的值。

联合体有以下特性:

  • 默认成员访问是public。
  • 修改一个成员会覆盖其他成员的值。
  • 联合体的对齐方式通常由其最大成员的对齐要求决定
  • 联合体的大小至少要能容纳其最大的数据成员,并且可能因对齐需求而增加额外空间。
union Example {
    char c;       // 1字节
    int i;        // 4字节(假设int为4字节)
    double d;     // 8字节(假设double为8字节)
}; // 8字节

匿名联合体是没有名称的联合体,其成员直接成为包含它的作用域的成员:

struct Employee {
    enum class Type { MANAGER, ENGINEER };
    Type type;
    
    union {
        char* department;  // 当type为MANAGER时使用
        int engineerId;    // 当type为ENGINEER时使用
    }; // 匿名联合体
    
    // 访问方式:直接通过Employee对象访问department或engineerId
};

// 使用示例
Employee e;
e.type = Employee::Type::MANAGER;
e.department = "HR";

7.4、this指针

this指针在特殊场景中的行为:

  • 在析构函数中使用this:析构函数执行时,对象的成员变量仍在内存中,但已进入销毁流程,析构函数执行完毕后,对象占用的内存会被回收。析构函数中不要访问已释放的成员变量,仅执行必要的资源释放操作,避免复杂逻辑,应避免调用其他成员函数。
class Resource {
public:
    ~Resource() {
        // 合法:析构函数执行时对象尚未完全销毁
        std::cout << "Destroying resource at " << this << std::endl;
        // 但不能调用非析构的成员函数,可能访问已释放内存
    }
};
  • delete this的危险用法
class Dangerous {
public:
    void selfDestruct() {
        std::cout << "Before delete: " << this << std::endl;
        delete this; // 调用后当前对象被销毁
        // this->data = 42;  // 灾难!访问已释放的内存
        // std::cout << data; // 同样危险
        // 应该立即返回
    }
private:
    int data;
};

Dangerous* obj = new Dangerous;
obj->selfDestruct();
// 从这里开始,obj已经成为悬空指针
// 任何对obj的使用都是未定义行为

这种模式有时用于引用计数对象的自我销毁

  • 多态场景下的this指针,在虚函数中,this指针始终指向实际对象(而非静态类型)
class Base {
public:
    virtual void printAddress() {
        std::cout << "Base: " << this << std::endl;
    }
};

class Derived : public Base {
public:
    void printAddress() override {
        Base::printAddress(); // 输出基类视角的this(与派生类相同)
        std::cout << "Derived: " << this << std::endl;
    }
};

// 输出:Base和Derived的this指针地址相同,指向同一对象

7.5、static成员

在类定义中,关键词static声明不绑定到类实例的成员

static成员变量是具有静态存储期的独立变量

class X { static int n; }; // 声明(用 'static')
int X::n = 1;              // 定义(不用 'static')

static成员函数不依赖于对象实例(没有this指针),只能访问static成员变量,可以直接用类名调用。

7.6、嵌套类

嵌套类(Nested Class)是指在一个类(称为外围类/外部类)内部定义的另一个类。嵌套类的作用域受外围类限制,但可以访问外围类的成员(包括私有成员),嵌套类被视为外部类的 “朋友”。。

嵌套类可以访问外部类的所有成员(包括私有、受保护和公共成员)以及成员函数,不过访问非静态成员时,必须借助外部类的实例来实现。对于外部类的静态成员,嵌套类可以直接访问,无需外部类的实例。

外部类无法直接访问嵌套类的私有和受保护成员,如需访问要将将外部类声明为友元类。

外部类和嵌套类之间成员的相互使用无需理会声明顺序:

class enclose
{
public:
	void printNest() {
		nested1 ne;
		ne.num = 10;
	}

private:
	class nested1 {
	public:
		void print() {
			enclose en;
			en.num = 10;
		}
	private:
		friend class enclose;
		int num;
	};

	int num;
};

嵌套类可以前置声明并在之后定义,在外围类的体内或体外均可:

class enclose
{
    class nested1;    // 前置声明
    class nested2;    // 前置声明
    class nested1 {}; // 嵌套类的定义
};
 
class enclose::nested2 { }; // 嵌套类的定义

嵌套类不影响外围类的大小。

7.7、派生类

在基类子句中列出的类是直接基类,直接基类的基类被称为间接基类。同一个类不能多次被指定为直接基类,但是可以既是直接基类又是间接基类。

基类子对象的构造函数被派生类的构造函数所调用:可以在成员初始化器列表中向这些构造函数提供实参。

继承时,如果省略访问说明符,那么它对以类关键词struct声明的类默认为public,对以类关键词class声明的类为private。

虚基类:虚继承是为了解决菱形继承问题而引入的。当使用虚继承时,无论通过多少条路径继承同一个虚基类,最终派生对象中只会包含该虚基类的一个实例。

struct B { int n; };
class X : public virtual B {};
class Y : virtual public B {};
class Z : public B {};
 
// 每个 AA 类型对象拥有一个 X,一个 Y,一个 Z 和两个 B:
// 一个是 Z 的基类,另一个由 X 与 Y 所共享
struct AA : X, Y, Z
{
    void f()
    {
        X::n = 1; // 修改虚 B 子对象的成员
        Y::n = 2; // 修改同一虚 B 子对象的成员
        Z::n = 3; // 修改非虚 B 子对象的成员
 
        std::cout << X::n << Y::n << Z::n << '\n'; // 打印 223
    }
};

上述例子虽然能运行,但是n的定义是不明确的!!

私有继承时,子类不对外表现出is-a的关系,但是可以在子类内部使用is-a,在类外部可以通过指针强转进行虚函数调用。

7.8、using 声明

在命名空间和块作用域中:using声明将另一命名空间的成员引入到当前命名空间或块作用域中。

在类定义中:using声明可以将别处定义的名字引入到此using声明所在的声明区中,例如将基类的受保护成员暴露为派生类的公开成员。这有几个优点:

7.8.1、实现接口扩展与兼容性

当你设计一个类的继承体系时,可能基类出于封装和安全性的考虑,将某些成员设置为受保护的。但在派生类的使用场景中,这些成员可能需要被外部更方便地访问,以满足特定的接口需求。通过 using 声明将基类的受保护成员提升为派生类的公开成员,可以在不破坏基类原有封装的前提下,为派生类提供更广泛的接口。

// 基类
class Base {
protected:
    void protectedFunction() {
        std::cout << "Base::protectedFunction() called" << std::endl;
    }
};

// 派生类
class Derived : public Base {
public:
    using Base::protectedFunction;  // 将基类的受保护成员暴露为公开成员
};

int main() {
    Derived d;
    d.protectedFunction();  // 可以直接调用,无需通过其他接口
    return 0;
}

引入有作用域枚举项:除了另一命名空间的成员和基类的成员,using声明也能将枚举的枚举项引入命名空间、块和类作用域。

7.8.2、继承构造函数

当在派生类中写下using Base::Base; 时,编译器会:

  • 隐式生成派生类的构造函数:这些构造函数与基类的构造函数具有相同的参数列表。
  • 将基类构造函数纳入重载决议:当创建派生类对象时,编译器会考虑基类的构造函数。
class Base {
public:
	Base(int a) { cout << "Base int " << endl; }
	Base(int a, double b) { cout << "Base int double " << endl; }
	Base(int a, double b, char c) { cout << "Base int double char " << endl; }
};

class Derived : public Base {
public:
	Derived(int a, double b) : Base(a, b){
		cout << "Derived int double " << endl;
	}
private:
	using Base::Base;  // 让Base的构造函数在Derived中可见
};

int main() {
	Derived a(1);
	Derived b(1, 1.1);
	Derived c(1, 2, 'c');
	return 0;
}

当创建派生类对象时,编译器会:

  • 优先考虑派生类自身定义的构造函数。
  • 如果没有匹配的构造函数,则考虑通过 using Base::Base; 继承的基类构造函数。
  • 继承的构造函数只会初始化基类部分,派生类的新增成员需通过默认初始化(如果有默认构造函数)或保持未初始化状态。

访问控制规则:

  • 基类构造函数的访问权限不变:如果基类的某个构造函数是 protected,继承后在派生类中仍然是 protected。
  • 派生类无法继承基类的私有构造函数:因为私有成员对派生类不可见。

构造函数继承的限制:

  • 无法初始化派生类成员
  • 无法继承默认 / 拷贝 / 移动构造函数
  • 冲突的构造函数会被删除:如果派生类已经定义了与基类构造函数参数列表相同的构造函数,继承的版本会被删除。

7.9、空基类优化

为保证同一类型的不同对象地址始终有别,要求任何对象或成员子对象的大小至少为1,即使该类型是空的类类型(即没有非静态数据成员的类或结构体),否则两个对象的大小为0,它们的内存地址可能相同,导致指针无法区分它们。

C++标准允许编译器将空基类子对象的大小优化为0字节,即使普通对象必须至少1字节

class Empty {};  // 空基类

class Derived : public Empty {
    int x;  // 只有一个数据成员
};

// 通常 sizeof(int) 为 4 字节
// 由于 EBCO,Empty 基类子对象的大小被优化为 0
// 因此 sizeof(Derived) == 4,而非 5!

网站公告

今日签到

点亮在社区的每一天
去签到