MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类与对象关系的深度剖析

2021-02-044.9k 阅读

C++类与对象关系的深度剖析

类的定义与结构

在C++中,类是一种用户自定义的数据类型,它将数据和操作数据的函数封装在一起。类的定义使用关键字class,基本语法如下:

class ClassName {
private:
    // 私有成员变量和函数
    int privateVariable;
    void privateFunction();
public:
    // 公有成员变量和函数
    int publicVariable;
    void publicFunction();
};

在上述代码中,ClassName是类的名称。类体被大括号包围,内部包含成员变量和成员函数。private关键字后面的成员只能在类的内部访问,而public关键字后面的成员可以在类的外部通过对象进行访问。

成员变量

成员变量是类的数据部分,它们定义了类所具有的属性。例如,定义一个Circle类来表示圆,可能会有一个成员变量radius来表示圆的半径:

class Circle {
private:
    double radius;
public:
    // 其他成员函数
};

成员变量可以是各种数据类型,包括基本数据类型(如intdouble等)和自定义数据类型(如其他类的对象)。

成员函数

成员函数是类的行为部分,它们定义了对类的数据进行操作的方法。在Circle类中,可以定义一个计算圆面积的成员函数calculateArea

class Circle {
private:
    double radius;
public:
    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

成员函数可以访问类的所有成员变量,无论其访问权限如何。

对象的创建与初始化

对象是类的实例,当定义了一个类后,可以通过创建对象来使用类的功能。

对象的创建

创建对象的语法与定义基本数据类型变量类似,例如:

Circle myCircle;

这里myCircle就是Circle类的一个对象。

构造函数与初始化列表

构造函数是一种特殊的成员函数,用于在对象创建时对其进行初始化。构造函数的名称与类名相同,没有返回类型(包括void)。在Circle类中,可以定义一个构造函数来初始化radius

class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {
        // 可以在这里添加其他初始化逻辑
    }
    double calculateArea() {
        return 3.14159 * radius * radius;
    }
};

上述代码中,Circle(double r) : radius(r)就是构造函数,使用了初始化列表的方式对radius进行初始化。初始化列表是一种更高效的初始化成员变量的方式,尤其适用于初始化那些具有复杂构造过程的成员变量,如引用类型或常量类型。

析构函数

析构函数与构造函数相反,用于在对象销毁时执行清理操作,例如释放动态分配的内存。析构函数的名称是在类名前加上波浪号~,同样没有返回类型和参数。例如:

class MyClass {
private:
    int* data;
public:
    MyClass() {
        data = new int[10];
    }
    ~MyClass() {
        delete[] data;
    }
};

在上述代码中,构造函数为data动态分配了内存,析构函数在对象销毁时释放了这片内存,避免了内存泄漏。

类与对象的访问权限

访问权限控制是C++类的重要特性,它决定了类的成员在何处可以被访问。

私有(private)访问权限

私有成员只能在类的内部访问,外部对象无法直接访问。例如,在Circle类中,radius是私有成员,不能在类的外部直接访问:

Circle myCircle(5.0);
// myCircle.radius = 10.0;  // 错误,radius是私有成员

这种访问限制确保了数据的安全性和封装性,外部代码只能通过类提供的公有接口(如成员函数)来间接访问和修改私有数据。

公有(public)访问权限

公有成员可以在类的内部和外部被访问。例如,calculateArea函数是公有成员,可以通过对象调用:

Circle myCircle(5.0);
double area = myCircle.calculateArea();

公有成员为外部代码提供了与类进行交互的接口,外部代码可以通过调用公有成员函数来操作对象的状态。

保护(protected)访问权限

保护成员类似于私有成员,只能在类的内部访问。但是,保护成员还可以被派生类(子类)访问。例如,定义一个Shape类和它的派生类Rectangle

class Shape {
protected:
    int x, y;
public:
    Shape(int a, int b) : x(a), y(b) {}
};
class Rectangle : public Shape {
public:
    Rectangle(int a, int b, int w, int h) : Shape(a, b), width(w), height(h) {}
    int calculateArea() {
        return width * height;
    }
private:
    int width, height;
};

在上述代码中,Shape类的xy是保护成员,Rectangle类作为Shape类的派生类,可以访问这些保护成员。

类的继承与多态

继承是面向对象编程的重要特性之一,它允许一个类从另一个类中获取属性和行为。

继承的基本概念

通过继承,一个类(称为派生类或子类)可以继承另一个类(称为基类或父类)的成员。例如,定义一个Animal基类和一个Dog派生类:

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};
class Dog : public Animal {
public:
    void bark() {
        std::cout << "Dog is barking." << std::endl;
    }
};

在上述代码中,Dog类继承自Animal类,因此Dog类对象可以调用Animal类的eat函数,同时也有自己的bark函数。

继承方式

继承方式决定了基类成员在派生类中的访问权限。常见的继承方式有公有继承(public)、私有继承(private)和保护继承(protected)。

  • 公有继承:基类的公有成员在派生类中仍然是公有成员,基类的保护成员在派生类中仍然是保护成员,基类的私有成员在派生类中不可访问。
  • 私有继承:基类的公有成员和保护成员在派生类中都变为私有成员,基类的私有成员在派生类中不可访问。
  • 保护继承:基类的公有成员和保护成员在派生类中都变为保护成员,基类的私有成员在派生类中不可访问。

多态性

多态性是指同一个函数调用在不同的对象上可以产生不同的行为。C++通过虚函数和指针或引用来实现多态。例如,在Animal类和Dog类的基础上,定义一个Cat类:

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks." << std::endl;
    }
};
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows." << std::endl;
    }
};

在上述代码中,Animal类的makeSound函数被声明为虚函数,Dog类和Cat类重写了这个虚函数。通过使用基类指针或引用来调用虚函数,可以实现多态:

Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound();  // 输出 "Dog barks."
animal2->makeSound();  // 输出 "Cat meows."
delete animal1;
delete animal2;

这里animal1animal2虽然是Animal类型的指针,但实际指向的是DogCat对象,调用makeSound函数时会根据对象的实际类型来执行相应的函数,从而实现多态。

类的其他特性

友元函数与友元类

友元函数和友元类是C++中打破类封装性的一种机制,它们允许外部函数或类访问类的私有和保护成员。

  • 友元函数:在类中使用friend关键字声明一个函数为友元函数,该函数就可以访问类的私有和保护成员。例如:
class MyClass {
private:
    int privateVariable;
public:
    MyClass(int value) : privateVariable(value) {}
    friend void printPrivateVariable(MyClass obj);
};
void printPrivateVariable(MyClass obj) {
    std::cout << "Private variable: " << obj.privateVariable << std::endl;
}
  • 友元类:同样使用friend关键字声明一个类为友元类,友元类的所有成员函数都可以访问原始类的私有和保护成员。例如:
class ClassB;
class ClassA {
private:
    int data;
public:
    ClassA(int value) : data(value) {}
    friend class ClassB;
};
class ClassB {
public:
    void accessData(ClassA obj) {
        std::cout << "Data in ClassA: " << obj.data << std::endl;
    }
};

友元机制虽然提供了灵活性,但过度使用会破坏类的封装性,应谨慎使用。

静态成员

静态成员是属于类而不是属于对象的成员。静态成员变量只有一份副本,被所有对象共享;静态成员函数可以在不创建对象的情况下被调用。

  • 静态成员变量:在类中使用static关键字声明,例如:
class Counter {
private:
    static int count;
public:
    Counter() {
        count++;
    }
    ~Counter() {
        count--;
    }
    static int getCount() {
        return count;
    }
};
int Counter::count = 0;

在上述代码中,count是静态成员变量,在类外进行初始化。每次创建Counter对象时,count会增加;每次销毁对象时,count会减少。

  • 静态成员函数:同样使用static关键字声明,例如getCount函数。静态成员函数只能访问静态成员变量,因为它不依赖于具体的对象实例。

运算符重载

运算符重载允许为自定义类定义运算符的行为。例如,为Complex类(表示复数)重载加法运算符+

class Complex {
public:
    double real, imag;
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
};

在上述代码中,operator+函数定义了Complex类对象相加的行为。可以像使用普通运算符一样使用重载后的运算符:

Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex result = c1 + c2;

运算符重载可以使代码更加直观和自然,提高代码的可读性。

类与对象在内存中的布局

了解类与对象在内存中的布局对于优化程序性能和理解程序行为非常重要。

非静态成员变量的内存布局

对于一个类的对象,其非静态成员变量按照声明顺序在内存中依次排列。例如,对于以下类:

class MyClass {
private:
    int a;
    double b;
    char c;
};

在内存中,a首先被分配4个字节(假设int为4字节),然后b被分配8个字节(double为8字节),最后c被分配1个字节。对象的总大小为4 + 8 + 1 = 13字节,但是由于内存对齐的原因,实际大小可能会大于13字节,通常会是8的倍数,例如16字节。

静态成员变量的内存布局

静态成员变量不存储在对象内部,而是存储在全局数据区。无论创建多少个对象,静态成员变量只有一份副本。例如,在Counter类中,count静态成员变量存储在全局数据区,与具体的Counter对象无关。

成员函数的内存布局

成员函数代码存储在代码段,不占用对象的内存空间。每个对象通过一个隐藏的指针this来访问成员函数,this指针指向对象自身,使得成员函数可以操作对象的成员变量。例如,在Circle类的calculateArea函数中,this指针隐式地指向调用该函数的Circle对象,从而可以访问对象的radius成员变量。

虚函数与虚表

当类中包含虚函数时,对象中会增加一个虚指针(vptr),它指向一个虚函数表(vtable)。虚函数表是一个数组,存储了类中虚函数的地址。当通过基类指针或引用调用虚函数时,程序会根据vptr找到对应的虚函数表,然后根据虚函数表中的地址调用实际的虚函数。例如,在Animal类及其派生类DogCat中,由于Animal类有虚函数makeSoundAnimalDogCat类的对象都会有一个vptrDogCat类的虚函数表中会存储它们重写后的makeSound函数的地址。

类与对象的高级应用

模板类

模板类是C++中一种通用的类定义方式,可以用于创建参数化类型的类。例如,定义一个简单的Stack模板类:

template <typename T>
class Stack {
private:
    T* data;
    int top;
    int capacity;
public:
    Stack(int cap) : capacity(cap), top(-1) {
        data = new T[capacity];
    }
    ~Stack() {
        delete[] data;
    }
    void push(T value) {
        if (top < capacity - 1) {
            data[++top] = value;
        }
    }
    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        return T();
    }
};

在上述代码中,typename T表示类型参数,在使用Stack类时,可以指定具体的类型,如Stack<int>Stack<double>

Stack<int> intStack(10);
intStack.push(10);
int value = intStack.pop();

模板类大大提高了代码的复用性,减少了重复代码的编写。

智能指针与对象管理

智能指针是C++中用于自动管理动态分配内存的工具,它可以有效避免内存泄漏。C++提供了std::unique_ptrstd::shared_ptrstd::weak_ptr三种智能指针。

  • std::unique_ptr:拥有对对象的唯一所有权,当std::unique_ptr销毁时,它所指向的对象也会被销毁。例如:
std::unique_ptr<MyClass> ptr1(new MyClass());
  • std::shared_ptr:允许多个指针共享对一个对象的所有权,通过引用计数来管理对象的生命周期。当引用计数为0时,对象被销毁。例如:
std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr3 = ptr2;
  • std::weak_ptr:是一种弱引用,它不增加对象的引用计数,主要用于解决std::shared_ptr的循环引用问题。例如:
std::shared_ptr<MyClass> ptr4 = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weakPtr = ptr4;

智能指针在现代C++编程中被广泛使用,尤其是在处理复杂的对象关系和动态内存分配时。

异常处理与对象生命周期

在C++中,异常处理机制可以用于处理程序运行过程中出现的错误。当异常抛出时,对象的生命周期管理变得尤为重要。例如,在一个函数中动态分配了对象并抛出异常:

void someFunction() {
    MyClass* obj = new MyClass();
    // 一些操作
    if (someErrorCondition) {
        delete obj;
        throw std::runtime_error("An error occurred.");
    }
    // 其他操作
    delete obj;
}

上述代码中,在抛出异常前需要手动释放obj的内存,否则会导致内存泄漏。使用智能指针可以简化这种情况:

void someFunction() {
    std::unique_ptr<MyClass> obj = std::make_unique<MyClass>();
    // 一些操作
    if (someErrorCondition) {
        throw std::runtime_error("An error occurred.");
    }
    // 其他操作
}

在这种情况下,当异常抛出时,std::unique_ptr会自动销毁MyClass对象,避免了内存泄漏。

通过深入了解C++类与对象的关系,包括类的定义、对象的创建与初始化、访问权限、继承、多态以及其他特性和高级应用,可以编写出更加健壮、高效和可维护的C++程序。在实际编程中,应根据具体需求合理运用这些知识,充分发挥C++面向对象编程的优势。同时,对类与对象在内存中的布局的理解也有助于优化程序性能,避免潜在的内存问题。